El mensaje se lee más o menos así, y la mitad que importa es el final:

Access to fetch at 'https://api.example.com/orders' from origin
'https://app.example.com' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.

Léelo como una frase sobre quién se negó. El servidor respondió. Tu petición salió del navegador, llegó a la otra máquina y volvió con una respuesta. Después el navegador se negó a entregar esa respuesta a tu JavaScript, porque la respuesta no decía que tu origen tuviera permiso para leerla.

Ese único hecho descarta casi todo lo que se prueba primero. No hay nada roto en la red. El servidor no está caído. No hay ningún cortafuegos por medio. La respuesta está en el navegador, y el navegador no la va a pasar.

Por qué el navegador hace esto

Sin ello, cualquier página que visites podría hacer peticiones en silencio a tu banco, a tu correo web o a las herramientas internas de tu empresa usando las cookies que ya están en tu navegador, y leer las respuestas.

Así que la regla es: JavaScript puede enviar una petición a otro origen, pero solo puede leer la respuesta si el servidor que responde dice que ese origen tiene permiso. El permiso tiene que venir del servidor al que se pregunta, porque es la única parte que sabe quién debería estar leyendo eso.

Dos consecuencias que sorprenden:

  • Solo se aplica al JavaScript del navegador. La misma petición desde curl, desde Postman o desde tu propio backend funciona, porque ninguno de ellos es un navegador custodiando la sesión de nadie. Una petición que funciona en Postman y falla en la página no es prueba de que el servidor esté roto.
  • Desactivarlo en tu navegador no arregla nada. Un flag o una extensión que apaga la comprobación hace que el error desaparezca en una máquina mientras todos los visitantes lo siguen viendo. Es una manera de confirmar el diagnóstico, no un arreglo, y usar el navegador así a diario es una idea genuinamente mala.

Leer qué regla ha fallado

El final del mensaje nombra la regla, y hay tres con las que te vas a encontrar.

No ‘Access-Control-Allow-Origin’ header is present. (No hay ninguna cabecera ‘Access-Control-Allow-Origin’.) El caso simple. El servidor no envió nada sobre permisos, así que el navegador da por hecho que no hay ninguno.

Response to preflight request doesn’t pass access control check. (La respuesta a la petición de preflight no pasa la comprobación de control de acceso.) El fallo ocurrió antes de que tu petición llegara a enviarse. Cualquier cosa que vaya más allá de una petición simple, un PUT o un DELETE, una cabecera propia como Authorization, un content type JSON, hace que el navegador envíe antes una petición OPTIONS preguntando si la de verdad está permitida. Si ese OPTIONS devuelve un 404, un 500, una redirección o un 200 sin las cabeceras correctas, la petición real no llega a ocurrir.

*The value of ‘Access-Control-Allow-Origin’ must not be the wildcard ‘’ when credentials mode is ‘include’.** (El valor no puede ser el comodín ‘*’ cuando se envían credenciales.) Estás enviando cookies, y * no es lo bastante específico para eso. El servidor tiene que nombrar tu origen exactamente y añadir Access-Control-Allow-Credentials: true.

Dónde va el arreglo

En el servidor que es dueño del recurso, en todos los casos. No en tu página, no en el navegador, no en un proxy que pongas delante de tu propio front end.

Si la API es tuya, eso significa devolver Access-Control-Allow-Origin con el origen que quieras permitir y, para los preflights, responder a OPTIONS con los métodos y las cabeceras permitidos y un estado 2xx. Si la API es de otro y no permite el acceso desde el navegador, entonces el acceso desde el navegador no está en oferta: llámala desde tu propio backend y deja que tu página hable con él.

Session Replay

Extensión gratuita de Chrome. Un clic en la página que se está portando mal captura la captura de pantalla, la consola y el registro de red, y te devuelve un enlace para pegar en el ticket.

Instalar la extensión

En localhost, que es donde casi todo el mundo se lo encuentra

http://localhost:3000 y http://localhost:8080 son orígenes distintos, así que un front end en un puerto llamando a una API en otro es una petición entre orígenes y se lleva el tratamiento completo.

La respuesta correcta en desarrollo es un proxy: tu servidor de desarrollo reenvía /api a la API, de modo que el navegador ve un solo origen y la cuestión nunca se plantea. Toda herramienta de front end moderna lo trae incorporado y cuesta una línea de configuración. La respuesta equivocada es arrancar Chrome con la seguridad web desactivada, porque entonces el código solo funciona para quien haya hecho lo mismo.

Lo que ve tu código

Nada útil, que es la parte que hace confuso depurar esto.

Una respuesta bloqueada no llega como un error que puedas inspeccionar. fetch() se rechaza con el genérico TypeError: Failed to fetch, sin estado y sin cuerpo, porque dejar que la página lea por qué la bloquearon filtraría justo la información que la regla existe para proteger.

Así que el mensaje de CORS de la consola no es un error que tu código haya capturado. Es el navegador diciéndote a ti, que programas, lo que ha hecho. Tu código no puede verlo, ni registrarlo, ni reportarlo.

Eso conviene saberlo cuando el aviso llega de otra persona. Un usuario que se topa con un fallo de CORS ve una funcionalidad que en silencio no hace nada, tu seguimiento de errores anota un Failed to fetch sin detalle, y la frase que lo habría explicado se imprimió en una consola que nadie guardó.

Cómo encontrarlo en un informe

Dos sitios, y quieres los dos.

La consola lleva el mensaje de CORS con el origen, la URL y la regla que falló. El panel de red muestra la petición en sí, y, en un fallo de preflight, la petición OPTIONS que está encima de la de verdad, con su propio estado. Abre sus cabeceras de respuesta: lo que falta ahí es todo el diagnóstico.

Si la petición no aparece en el panel de red en absoluto, el navegador se negó antes de enviarla, lo que apunta a contenido mixto o a una extensión y no a CORS.

La versión corta

  • El servidor respondió; el navegador se negó a dejar que tu código leyera la respuesta.
  • Solo se aplica al JavaScript del navegador, así que curl y Postman no prueban nada.
  • El final del mensaje nombra qué regla ha fallado. Lee eso, no el principio.
  • Los fallos de preflight ocurren antes de tu petición, en un OPTIONS que el navegador envió por ti.
  • El arreglo va en el servidor que responde. Desactivar la comprobación en local es un diagnóstico, no un arreglo.
  • En localhost, usa un proxy de desarrollo para que solo haya un origen.