---
title: "Error CORS: qué es «blocked by CORS policy» y su solución"
description: "Un error CORS significa que el navegador no dejó a tu página leer la respuesta de otro origen. Te explicamos qué es blocked by CORS policy y cómo solucionarlo."
url: https://proxynet.io/es/blog/cors-error
date: 2026-10-06
author: "Acar Diveroli"
category: "Tutoriales, Web scraping"
lang: es
---

# Error CORS: qué es «blocked by CORS policy» y su solución

Tu frontend se ejecuta en `http://localhost:5173` y tu API en `http://localhost:3001`. La API devuelve JSON en curl y en Postman, pero en el navegador `fetch()` lanza `TypeError: Failed to fetch` y la consola muestra: `Access to fetch at 'http://localhost:3001/api/products' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.` Sin embargo, el log de la API indica que la petición llegó y se respondió con `200`.

A continuación: qué es un origen, por qué el navegador bloquea la respuesta, las peticiones simples frente a las peticiones preflight, cada mensaje de Chrome, soluciones probadas para Express, Flask y Vite, una ruta en el backend para las API que no controlas y por qué el código de scraping nunca se topa con CORS.

> **Nota: Respuesta breve**
>
> Un error CORS significa que el navegador recibió una respuesta de otro origen, pero no quiso entregársela a tu JavaScript, porque el servidor no envió una cabecera `Access-Control-Allow-Origin` con el origen de tu página. Un origen es el esquema, el host y el puerto juntos, así que `localhost:5173` y `localhost:3001` son orígenes distintos. Corrígelo en el servidor: permite tu origen exacto, responde a las peticiones preflight `OPTIONS` y enumera los métodos y cabeceras que usas. En desarrollo, un proxy del servidor de desarrollo pone la API en el mismo origen; una API que no es tuya, llámala desde tu backend. `mode: "no-cors"`, las extensiones del navegador, los proxies CORS públicos, las VPN y los proxies no lo arreglan.

## ¿Qué es un error CORS?

Es el navegador negándose a entregar una respuesta a tu script. CORS, Cross-Origin Resource Sharing (intercambio de recursos entre orígenes), es un conjunto de cabeceras de respuesta HTTP con las que un servidor le dice al navegador qué otros orígenes pueden leer sus respuestas. El [Fetch Standard](https://fetch.spec.whatwg.org/#http-cors-protocol) lo define como un permiso que el servidor tiene que activar de forma explícita, para que los datos que están detrás de un firewall o de un inicio de sesión no se filtren por defecto a otros sitios. Sin cabeceras que coincidan, el navegador aplica su regla por defecto, la política del mismo origen (same-origin policy), y bloquea la lectura.

De ahí salen tres cosas. Los logs del servidor parecen normales, porque el error lo produjo el navegador. Tu código solo recibe un fallo genérico, `TypeError: Failed to fetch` desde `fetch()` o `AxiosError: Network Error` (código `ERR_NETWORK`) desde Axios, y el motivo solo aparece en la consola. Y la solución está en el servidor que responde, no en el código que pregunta.

## ¿Qué es un origen y qué bloquea la política del mismo origen?

Un origen (origin) es el esquema, el host y el puerto de una URL. Dos URL comparten origen solo cuando coinciden los tres:

- `http://localhost:5173` y `http://localhost:3001`: puertos distintos, orígenes distintos.
- `http://example.com` y `https://example.com`: esquemas distintos.
- `https://example.com` y `https://api.example.com`: hosts distintos; un subdominio es un origen propio.
- `https://example.com/shop` y `https://example.com/api`: mismo origen; la ruta no cuenta.

La política del mismo origen sigue permitiendo que una página incruste imágenes, scripts y hojas de estilo de otros sitios, enlace a ellos y les envíe formularios. Lo que impide es la lectura: un script no puede leer una respuesta de otro origen salvo que ese origen lo permita ([MDN: Same-origin policy](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy)). CORS es la forma en que el servidor concede ese permiso.

## ¿Cómo funciona paso a paso una petición entre orígenes?

1. Tu página en el origen A llama a `fetch()` para una URL del origen B.
2. El navegador comprueba si la petición es «simple». Si no lo es, primero envía un preflight (siguiente sección).
3. Añade una cabecera `Origin`, como `Origin: http://localhost:5173`. Los scripts no pueden fijarla ni quitarla.
4. El servidor ejecuta su código y responde. Lo que haya hecho el código ya ha ocurrido.
5. El navegador compara el `Access-Control-Allow-Origin` de la respuesta con el origen de la página. Acepta una coincidencia exacta, o `*` cuando no se enviaron credenciales como las cookies.
6. Si coinciden, tu código recibe la respuesta; si no, el navegador la descarta, la promesa se rechaza y la consola indica el motivo.

El paso 4 es el que se pasa por alto: CORS no impide que las peticiones lleguen al servidor, y curl, los scripts y otros servidores se saltan el paso 5 por completo. Protege a los visitantes, para que una página que abren no pueda leer sus datos en otros sitios con sus cookies; no protege tu API.

## Peticiones simples y preflight: por qué GET funciona y POST falla

Una petición es «simple» cuando usa `GET`, `HEAD` o `POST` y solo cabeceras de la lista segura: `Accept`, `Accept-Language`, `Content-Language`, `Range` y `Content-Type` con `application/x-www-form-urlencoded`, `multipart/form-data` o `text/plain`. Todo lo demás pasa antes por un preflight (comprobación previa): una petición `OPTIONS` con `Access-Control-Request-Method` y `Access-Control-Request-Headers`, tras la cual el navegador envía la petición real solo si la respuesta lo permite ([MDN: CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS)). Un cuerpo JSON, una cabecera `Authorization`, `PUT`, `PATCH`, `DELETE` o una cabecera personalizada como `X-Request-ID` lo activan.

Enviamos un `GET` simple y un `POST` con cuerpo JSON desde una página en el puerto 5173 a una API en el puerto 3001 que no envía cabeceras CORS. La API registró:

```text
[no-cors-api] GET /api/products origin=http://localhost:5173
[no-cors-api] OPTIONS /api/products origin=http://localhost:5173
```

El `GET` se respondió con `200` y los datos, y aun así el navegador lo bloqueó. Del `POST` solo llegó el preflight; falló, así que el `POST` nunca se envió. En cambio, un `POST` simple entre orígenes, como uno codificado como formulario, sí llega al servidor y se ejecuta aunque la página vea un error. Por eso conviene proteger esas acciones con autenticación y tokens CSRF, no con CORS.

`Access-Control-Max-Age` permite al navegador guardar en caché la respuesta a un preflight. El valor por defecto del Fetch Standard es de 5 segundos; Chromium limita el valor a 2 horas y Firefox a 24 horas.

## ¿Qué significa cada mensaje «blocked by CORS policy»?

Todos los mensajes de Chrome empiezan por `Access to fetch at '<URL>' from origin '<origin>' has been blocked by CORS policy:`, o por `Access to XMLHttpRequest at` con Axios y `XMLHttpRequest`. Chromium deja estos textos fijos en inglés en su código fuente, así que aparecen en inglés también en un navegador en español. El texto después de los dos puntos es la causa. Las cinco primeras filas de la tabla aparecieron palabra por palabra en nuestra prueba con un navegador basado en Chromium 152; el resto sale del código fuente de Chromium.

| Mensaje tras «blocked by CORS policy:» | Significado | Solución |
|---|---|---|
| `No 'Access-Control-Allow-Origin' header is present on the requested resource.` | Falta la cabecera CORS: no está configurada o respondió una página de error | Permite tu origen; comprueba el estado real |
| `Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present…` | La respuesta a `OPTIONS` no tenía cabecera CORS; la petición real no se envió | Responde a `OPTIONS` con cabeceras CORS |
| `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'.` | Se enviaron cookies y el servidor respondió `*` | Origen exacto más `Access-Control-Allow-Credentials: true` |
| `Request header field x-debug is not allowed by Access-Control-Allow-Headers in preflight response.` | Una cabecera que envías no está permitida | Permítela o deja de enviarla |
| `Method PATCH is not allowed by Access-Control-Allow-Methods in preflight response.` | El método no está permitido | Añade el método |
| `The 'Access-Control-Allow-Origin' header has a value '…' that is not equal to the supplied origin.` | Se permite otro origen: puerto equivocado, `http`, una barra final | Corrige la lista de permitidos |
| `The 'Access-Control-Allow-Origin' header contains multiple values '…', but only one is allowed.` | Dos capas añaden la cabecera, como la app y nginx | Defínela en un solo sitio |
| `Response to preflight request doesn't pass access control check: It does not have HTTP ok status.` | `OPTIONS` recibió `401`, `404`, `405` o `500` | Deja pasar `OPTIONS` antes de la autenticación |
| `Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.` | `OPTIONS` se redirigió, por ejemplo a `https` o a una página de inicio de sesión | Llama a la URL final |

Las versiones antiguas de Chrome añadían una frase que sugería `mode: 'no-cors'`; Chromium la eliminó en marzo de 2025 porque confundía a la gente (las preguntas frecuentes explican por qué). Firefox escribe `Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at … (Reason: CORS header 'Access-Control-Allow-Origin' missing)`.

## ¿Cómo encontrar la causa real?

Abre las herramientas para desarrolladores (F12) y lee la línea de la pestaña **Console** (**Consola** en la interfaz en español): URL, origen, motivo. En la pestaña **Network** (**Red**), la petición bloqueada muestra `CORS error` (`Error CORS`) en la columna **Status** (**Estado**), y una llamada con preflight tiene una entrada `OPTIONS` aparte cuya columna **Initiator** (**Iniciador**) dice `Preflight` (`Solicitud preparatoria` en español). Haz clic en ella para ver las cabeceras.

El código de estado real es fácil de pasar por alto. En nuestra prueba, una puerta de enlace (gateway) respondió `502` sin cabeceras CORS: la consola solo mostró la línea «No 'Access-Control-Allow-Origin' header», mientras que `curl -i` contra la misma URL imprimió `HTTP/1.1 502 Bad Gateway`. Las páginas de error de nginx, de los balanceadores de carga o de una app caída rara vez llevan cabeceras CORS, así que las caídas parecen problemas de CORS. La directiva `add_header` de nginx solo se aplica a las respuestas 200, 201, 204, 206, 301, 302, 303, 304, 307 y 308, salvo que añadas el parámetro `always` ([documentación de nginx](https://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header)).

## ¿Cómo solucionar un error CORS si la API es tuya?

Envía las cabeceras desde la propia API, para los orígenes exactos de tu frontend. Probamos cada fragmento el 6 de octubre de 2026 con Node.js 24.11, Express 5.2.1, cors 2.8.6, Vite 8.3.3, Python 3.13, Flask 3.1.3 y Flask-CORS 6.0.5.

### Express

Instala con `npm install express cors` y pon `"type": "module"` en `package.json`.

```js
// server.js: una API que permite un único frontend (Express 5, cors 2.8)
import express from "express";
import cors from "cors";

const app = express();

app.use(cors({
  origin: ["https://app.example.com", "http://localhost:5173"],
  methods: ["GET", "POST", "PUT", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization"],
  credentials: true,
  maxAge: 600,
}));
app.use(express.json());

app.get("/api/products", (req, res) => {
  res.json([{ id: 1, name: "Desk lamp" }]);
});

app.post("/api/products", (req, res) => {
  res.status(201).json({ created: req.body });
});

app.listen(3000, () => console.log("API on http://localhost:3000"));
```

Registrado con `app.use()` antes de las rutas, el middleware también responde a cada preflight `OPTIONS` ([middleware cors de Express](https://expressjs.com/en/resources/middleware/cors.html)). Escribe los orígenes exactamente como los envía el navegador, sin barra final. Los demás orígenes no reciben cabecera `Access-Control-Allow-Origin`, y de eso se trata. Mantén `credentials: true` solo si el frontend envía cookies con `credentials: "include"`.

Comprueba el preflight desde una terminal:

```bash
curl -i -X OPTIONS http://localhost:3000/api/products \
  -H "Origin: http://localhost:5173" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type"
```

Las líneas relevantes de nuestra ejecución:

```text
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:5173
Vary: Origin
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,POST,PUT,DELETE
Access-Control-Allow-Headers: Content-Type,Authorization
Access-Control-Max-Age: 600
```

`Vary: Origin` indica a las cachés y a las CDN que la respuesta depende de la cabecera `Origin`. Con `Origin: https://evil.example`, el mismo comando devuelve `204`, pero sin la línea `Access-Control-Allow-Origin`.

### Flask

Instala con `pip install flask flask-cors`.

```python
# app.py: la misma API en Flask 3 con Flask-CORS 6
from flask import Flask, jsonify, request
from flask_cors import CORS

app = Flask(__name__)
CORS(
    app,
    resources={r"/api/*": {"origins": ["https://app.example.com", "http://localhost:5173"]}},
    allow_headers=["Content-Type", "Authorization"],
    supports_credentials=True,
    max_age=600,
)

@app.get("/api/products")
def list_products():
    return jsonify([{"id": 1, "name": "Desk lamp"}])

@app.post("/api/products")
def create_product():
    return jsonify({"created": request.get_json()}), 201

if __name__ == "__main__":
    app.run(port=5000)
```

En nuestra ejecución, el preflight devolvió `200` con el origen, los métodos y `Vary: Origin`, y el `POST` de la página devolvió `201`. Pasa siempre una lista de orígenes con `supports_credentials=True`: sin ella, Flask-CORS 6.0.5 nos devolvió tal cual un origen arbitrario, `https://evil.example`, junto con `Access-Control-Allow-Credentials: true`.

### nginx, puertas de enlace y CDN

Si un proxy inverso (reverse proxy) pone las cabeceras en lugar de la app, añade a cada `add_header` el parámetro `always` para que las respuestas de error también las lleven, y deja que solo una capa las defina. Nunca copies cualquier `Origin` entrante en la respuesta mientras permites credenciales: cualquier sitio web podría entonces leer los datos de tus usuarios con sus cookies. Compara los orígenes con una lista fija.

## Desarrollo local: deja que el servidor de desarrollo reenvíe la API

En desarrollo, la solución más sencilla es poner la API en el mismo origen. El servidor de desarrollo de Vite puede reenviar cada ruta que empiece por `/api`:

```js
// vite.config.js: en desarrollo, /api pasa por Vite hasta la API
import { defineConfig } from "vite";

export default defineConfig({
  server: {
    port: 5173,
    proxy: {
      "/api": {
        target: "http://localhost:3000",
        changeOrigin: true,
      },
    },
  },
});
```

El frontend llama a `fetch("/api/products")` sin host. El navegador ve su propio origen, así que no hay comprobación CORS, y Vite pasa la petición al puerto 3000; en nuestra prueba `GET` devolvió `200` y `POST` `201`. `changeOrigin` pone el destino en la cabecera `Host`. webpack-dev-server ofrece lo mismo con `devServer.proxy`. En producción, sirve el frontend y la API bajo un mismo origen a través de tu servidor web, o mantén la configuración CORS de arriba.

## Si la API no es tuya: llámala desde tu backend

Una API de terceros sin cabeceras CORS suele estar pensada para servidores, a menudo porque su clave secreta nunca debe llegar a un navegador. Añade una ruta a tu backend: el navegador llama a tu origen y tu servidor llama a la API.

```js
// relay.js: tu backend llama a la API de terceros; el navegador solo habla contigo
import express from "express";

const app = express();
const PARTNER_URL = "https://api.partner.example/v1/products";

app.get("/api/partner-products", async (req, res) => {
  try {
    const r = await fetch(PARTNER_URL, {
      headers: { Authorization: `Bearer ${process.env.PARTNER_API_KEY}` },
      signal: AbortSignal.timeout(10_000),
    });
    res.status(r.status).type(r.headers.get("content-type") ?? "application/json");
    res.send(await r.text());
  } catch (err) {
    res.status(502).json({ error: "partner API unreachable", detail: err.cause?.code ?? err.name });
  }
});

app.listen(8080, () => console.log("relay on http://localhost:8080"));
```

Contra un sustituto local de la API del socio sin cabeceras CORS, la ruta devolvió el JSON con `200`. Sirve la ruta desde el origen de la página; la clave se queda en una variable de entorno del servidor. Las cabeceras, los cuerpos y la autenticación con `fetch` y Axios en el lado del servidor se explican en [cURL en JavaScript](/es/blog/curl-in-javascript).

Mantén el destino fijo. Una ruta que pide cualquier cosa que llegue en `?url=` convierte tu servidor en un proxy abierto que cualquiera puede usar, incluso contra direcciones internas a las que solo llega tu servidor. Los términos de uso y los límites de solicitudes de la API siguen vigentes: la llamada cambió de sitio, las reglas no.

## Por qué los proxies CORS públicos son un riesgo

Un proxy CORS público pide una URL por ti y añade `Access-Control-Allow-Origin: *` a la respuesta. El error desaparece, pero:

- El operador ve la URL completa, todas las cabeceras (incluidas las claves de API y los tokens) y la respuesta.
- Puede modificar la respuesta, y tu página confiará en ella.
- Las peticiones de tus usuarios pasan por una empresa con la que no tienes ningún acuerdo.
- Sus límites de solicitudes y sus caídas pasan a ser los tuyos.

## CORS y web scraping: por qué tu script de Python o Node.js nunca lo ve

CORS solo existe en los navegadores. Pedimos la misma API que el navegador había bloqueado, esta vez desde Node.js 24:

```js
// La misma petición desde Node.js: sin navegador, sin comprobación CORS
const r = await fetch("http://localhost:3001/api/products");
console.log(r.status, r.headers.get("access-control-allow-origin"), await r.text());
```

```text
200 null [{"id":1,"name":"Desk lamp"}]
```

No hay cabecera `Access-Control-Allow-Origin`, y Node.js lee la respuesta igualmente; Requests de Python y curl se comportan igual. Si intentas recopilar datos con `fetch()` en la consola del navegador o en una app de frontend, el error CORS te está diciendo que ese trabajo corresponde a código del lado del servidor. Qué lenguaje encaja mejor se compara en [Web scraping: ¿JavaScript o Python?](/es/blog/web-scraping-javascript-vs-python). Las reglas del sitio siguen valiendo ahí: respeta `robots.txt`, mantén un ritmo bajo de peticiones y usa la API oficial cuando exista.

Un proxy no arregla un error CORS. El navegador compara el origen de la página con la cabecera `Access-Control-Allow-Origin`, y la dirección IP no forma parte de esa comprobación, así que un proxy residencial, un proxy de centro de datos o una VPN no cambian nada. Los proxies van en el cliente HTTP de tu código del lado del servidor, como se muestra en [Cómo usar un proxy en Node.js](/es/blog/nodejs-proxy).

Para una recopilación permitida que necesita resultados locales en muchos países y ciudades, los [Proxies residenciales](https://proxynet.io/es/residential-proxy) envían las peticiones a través de conexiones domésticas, con segmentación por país y ciudad.

Para grandes volúmenes de peticiones a API públicas y páginas con poca protección, los [Proxies de centro de datos](https://proxynet.io/es/datacenter-proxy) ofrecen direcciones IPv4 dedicadas con tráfico sin cuota.

## ¿Quién se encuentra con errores CORS?

- Desarrolladores de frontend cuya app y cuya API se ejecutan en puertos distintos.
- Aplicaciones de una sola página que llaman a una API de terceros directamente desde el navegador.
- Equipos que mueven una API a un subdominio como `api.example.com`, o a `https`.
- Frontends generados con IA que llaman a una API directamente, a veces con la clave dentro de la página.

## Errores comunes

- **Añadir `Access-Control-Allow-Origin` a la petición.** Es una cabecera de respuesta; en `fetch()` se convierte en una cabecera personalizada que activa un preflight.
- **Usar `mode: "no-cors"`.** La respuesta vuelve opaca: estado `0`, cuerpo ilegible.
- **Una extensión «allow CORS» o la seguridad web desactivada.** Solo cambia tu propio navegador, y una extensión así puede leer las páginas en las que se ejecuta.
- **Responder `*` mientras el frontend envía cookies.** El navegador rechaza esa combinación.
- **`app.options("*", cors())` en Express 5.** La app se detiene al arrancar con `PathError [TypeError]: Missing parameter name at index 1: *`; `app.use(cors())` ya responde a los preflights.
- **Un middleware de autenticación que rechaza `OPTIONS`.** Los preflights no llevan token, así que fallan con «It does not have HTTP ok status».

## Guía de decisión

| Situación | Qué hacer |
|---|---|
| Frontend y API en puertos distintos durante el desarrollo | Proxy del servidor de desarrollo (`server.proxy` en Vite) |
| Tu API, llamada desde tu frontend | Permite los orígenes exactos; responde a `OPTIONS` |
| El frontend envía cookies | Origen exacto, `Access-Control-Allow-Credentials: true`, `Vary: Origin` |
| Una cabecera o un método «is not allowed» | Añádelo a `Access-Control-Allow-Headers` o `-Methods` |
| El error aparece tras un despliegue o bajo carga | Comprueba el estado real con curl; `always` en nginx |
| Una API de terceros sin cabeceras CORS | Llámala desde tu backend, con la clave en el servidor |
| Recopilar datos de otros sitios | Código del lado del servidor; ahí CORS no se aplica |

## Preguntas frecuentes

### ¿Por qué funcionan Postman o curl mientras el navegador muestra un error CORS?

Solo los navegadores aplican CORS. El servidor envía la misma respuesta a cualquier cliente, y solo el navegador comprueba `Access-Control-Allow-Origin` antes de entregársela a un script. Que curl funcione demuestra que la API está en marcha, no que un navegador pueda leer la respuesta.

### ¿Por qué me sale un error CORS en localhost?

Un puerto distinto es un origen distinto: `localhost:5173` y `localhost:3000` son dos orígenes en una misma máquina. Permite el origen del frontend en la API o usa el proxy del servidor de desarrollo. Aparte de eso, desde Chrome 142 un sitio web público que llama a `localhost` o a un dispositivo de tu red doméstica necesita tu permiso; si lo rechazas, la consola informa de un espacio de direcciones `loopback` o `local` denegado.

### ¿Usar mode: "no-cors" soluciona un error CORS?

No. La petición se envía, pero el navegador devuelve una respuesta opaca; en nuestra prueba el estado fue `0`, el tipo `opaque` y el cuerpo ilegible. Solo sirve para peticiones cuya respuesta nunca vas a leer.

### ¿CORS es una medida de seguridad para mi API?

No. Impide que un sitio lea los datos de otro sitio con las cookies de un visitante, pero cualquiera puede seguir llamando a tu API con un script. Protege la API con autenticación, autorización y límites de solicitudes.

### ¿Un proxy o una VPN pueden arreglar un error CORS?

No. El navegador compara el origen de la página con la cabecera `Access-Control-Allow-Origin` de la respuesta; la dirección IP no forma parte de la comprobación.

### ¿Por qué solo falla mi petición POST mientras GET funciona?

Probablemente el `POST` necesita un preflight por un cuerpo JSON, una cabecera `Authorization` o una cabecera personalizada. Si la API no responde bien a la petición `OPTIONS`, el navegador nunca envía el `POST`. Busca la entrada `OPTIONS` en la pestaña Network (Red) o envía el preflight con curl.

## En resumen

Un error CORS es el navegador negándose a entregar a tu script la respuesta de otro origen; normalmente la petición sí llegó al servidor. Lee el motivo en la consola, comprueba el estado real con curl y corrige el servidor: orígenes exactos, una respuesta a `OPTIONS`, los métodos y cabeceras correctos, `Vary: Origin`. Usa el proxy del servidor de desarrollo mientras desarrollas y tu propio backend para las API que no controlas. La recopilación de datos corresponde al código del lado del servidor, donde CORS no se aplica; los tipos de proxy para ese trabajo están en nuestra página de [servicios de proxy](/es/proxy).
