viernes, mayo 02, 2025

Angular, proxy y CORS

La eterna batalla contra CORS: Un recordatorio para mi yo del futuro

De vez en cuando, sobre todo en nuevos proyectos, me tropiezo con la misma piedra. Y hoy, después de perder medio día entre búsquedas en Google, posts antiguos en Stack Overflow, y consultas a Perplexity, ChatGPT 4o-mini, Claude y Deepseek, he decidido documentar la solución para Juan Pablo del futuro.

El problema recurrente

Usualmente, en mis proyectos con Angular (v18), configuro mis variables de ambiente en environment.ts y environment.prod.ts. Típicamente como algo así:

export const environment = {
  production: false,
  protocol: 'http',
  baseUrl: 'http://localhost:5000/',
};

Y luego tengo un servicio que puede hacer una petición al backend así:

  // Use baseUrl from environment and append the specific API path segment
  private apiUrl = environment.baseUrl + 'api/v1'; // Ensure this points to your Go backend

  // Fetch subscription status from API and update the BehaviorSubject
  loadSubscriptionStatus(): Observable<SubscriptionStatus> {
    const headers = this.getAuthHeaders();
    return this.http.get<SubscriptionStatus>(`${this.apiUrl}/subscriptions/current`, { headers })
      .pipe(
        tap(status => this.subscriptionStatusSubject.next(status)), // Update state on success
        catchError(err => {
          this.subscriptionStatusSubject.next(null); // Clear state on error
          return this.handleError(err); // Propagate error
        })
      );
  }

El dolor de cabeza familiar

Y por supuesto, la primera vez el navegador me responde con un error:

Access to XMLHttpRequest at 'http://localhost:5000/api/v1/subscriptions/current' from origin 'http://localhost:4200' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

Me duele la cabeza cada vez que veo ese error... y recuerdo: "¡Ah, tengo que configurar un proxy!". Mientras desarrollo es buena idea, ya en producción no será necesario. Y entonces busco un proyecto anterior y copio el archivo proxy.conf.json y lo acomodo en el angular.json para que se lea cada vez que corro ng serve.

{
    "/api": {
        "target": "http://localhost:5000",
        "secure": false,
        "logLevel": "debug",
        "changeOrigin": false
    }
}

Pero el error persiste y pierdo la mitad del día entre búsquedas, todos dan consejos razonables, todos los pruebo cada vez con menos entusiasmo, y me digo: "esto ya lo viví cientos de veces".

La solución (otra vez)

Lo acabo de resolver, y ahora lo voy a documentar para Juan Pablo del futuro.

Toda solución parece evidente cuando la encuentras. Explorando vi algo que podría ser la falla...

Mi baseURL está apuntando al backend, y tengo un proxy que también lo hace. Uno de los dos está mal, de lo contrario no necesitaría el proxy.

Así que, modificando el baseUrl a http://localhost:4200 —el puerto original de Angular— se solucionó el problema.

Cabe mencionar que también configuré el servicio para soportar CORS:

	// Register CORS middleware to allow Angular frontend requests
	pb.OnBeforeServe().Add(func(e *core.ServeEvent) error {
		app.Log.Info("CORS middleware ---")
		e.Router.Use(middleware.CORSWithConfig(middleware.CORSConfig{
			AllowOrigins:     []string{"http://localhost:4200", "http://localhost:8090"},
			AllowHeaders:     []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization},
			AllowMethods:     []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete, http.MethodOptions},
			AllowCredentials: true,
			// Ensure OPTIONS requests are handled correctly
			MaxAge: 86400, // Optional: cache preflight results for 24 hours
		}))

		return nil
	})

Esto ya lo había implementado antes eh, no creas que no estaba cuando el navegador se rehusaba a la conexión pero lo pongo ahora por si alguien pensó que me faltaba.

Lección aprendida (otra vez)

La moraleja es simple: cuando uses un proxy para desarrollo, asegúrate de que tu baseUrl apunte al servidor de desarrollo de Angular (localhost:4200), no al backend directamente. El proxy se encargará de redirigir las peticiones al backend apropiadamente.

export const environment = {
  production: false,
  protocol: 'http',
  baseUrl: 'http://localhost:4200/',  // <-- aquí fíjate!
};

Espero que este recordatorio me ahorre tiempo la próxima vez que me encuentre maldiciendo al error de CORS.

No hay comentarios.: