Los errores CORS son uno de los problemas más frustrantes en el desarrollo web. Ves el temido mensaje rojo en la consola, tu llamada API falla, y nada parece funcionar. Esta guía te ayudará a entender exactamente qué es CORS, diagnosticar tu error específico y solucionarlo con la solución correcta.
¿Qué es CORS? (Explicación en 30 segundos)
Cross-Origin Resource Sharing (CORS) es una función de seguridad del navegador que bloquea las páginas web para que no realicen peticiones a un dominio diferente al que sirve la página. Cuando tu frontend en http://localhost:3000 intenta obtener datos de http://api.example.com, el navegador verifica si el servidor API permite explícitamente esta petición cross-origin mediante cabeceras HTTP especiales.
Cómo funciona CORS: el flujo
┌─────────────────┐ ┌─────────────────────┐
│ Browser │ │ API Server │
│ (Frontend) │ │ (Backend) │
│ │ │ │
│ http://localhost │ ──────> │ https://api.example │
│ :3000 │ fetch │ .com │
│ │ │ │
└─────────────────┘ └─────────────────────┘
│
┌───────────────┴───────────────┐
│ │
CORS Headers No CORS Headers
Present? Present?
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ Response sent │ │ Browser BLOCKS │
│ to JavaScript │ │ the response │
└─────────────────┘ └──────────────────┘Cuando CORS está configurado correctamente, el servidor responde con cabeceras que le dicen al navegador: "Sí, permito peticiones de este origen."
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Content-Type: application/json
{"data": "success"}Cuando CORS NO está configurado, el navegador bloquea la respuesta (aunque el servidor haya procesado la petición correctamente).
HTTP/1.1 200 OK
Content-Type: application/json
# No Access-Control-Allow-Origin header!
{"data": "success"} <-- Browser received this but BLOCKS JavaScript from reading itLos 5 mensajes de error CORS más comunes
Error 1: Sin cabecera Access-Control-Allow-Origin
No 'Access-Control-Allow-Origin' header is present on the requested resource.Significado: El servidor no incluyó la cabecera Access-Control-Allow-Origin. Es el error CORS más común.
Error 2: Wildcard con credenciales
The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*' when credentials mode is 'include'.Significado: Estás enviando cookies o cabeceras Authorization, pero el servidor responde con *. Con credenciales, el servidor debe especificar el origen exacto.
Error 3: Petición preflight fallida
Response to preflight request doesn't pass access control check.Significado: El navegador envió una petición OPTIONS (preflight) y el servidor no la manejó correctamente.
Error 4: Método no permitido
Method PUT is not allowed by Access-Control-Allow-Methods.Significado: La cabecera Access-Control-Allow-Methods del servidor no incluye el método HTTP que estás usando.
Error 5: Cabecera no permitida
Request header field X-Custom-Header is not allowed by Access-Control-Allow-Headers.Significado: El servidor no ha permitido la cabecera personalizada en Access-Control-Allow-Headers.
Diagrama de diagnóstico: ¿dónde está el problema?
Usa este árbol de decisión para identificar rápidamente si tu problema CORS es frontend, backend o proxy:
Paso 1: Abre DevTools > pestaña Network. ¿Ves una petición OPTIONS antes de tu petición?
SÍ -> El navegador envía una petición preflight. Verifica si la respuesta OPTIONS tiene estado 200 y contiene cabeceras CORS.
NO -> Es una petición simple. El problema es que la respuesta del servidor no tiene cabeceras CORS.
Paso 2: ¿El error menciona "preflight"?
SÍ -> Problema de backend. Tu servidor necesita manejar peticiones OPTIONS.
NO -> Verifica si usas credenciales. Si es así, no puedes usar el wildcard (*).
Paso 3: ¿La API funciona al llamarla directamente (curl, Postman)?
SÍ -> Confirmado que es un problema CORS (solo navegador).
NO -> NO es un problema CORS. La API misma tiene un problema.
Resumen: El 90% de los errores CORS se resuelven configurando las cabeceras correctas en tu servidor backend.
Correcciones del lado del servidor por framework
Express.js (Node.js)
// Method 1: Using the cors package (recommended)
npm install cors
const express = require('express');
const cors = require('cors');
const app = express();
// Allow specific origins
app.use(cors({
origin: ['http://localhost:3000', 'https://yoursite.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400 // Cache preflight for 24 hours
}));
// Method 2: Manual middleware
app.use((req, res, next) => {
const allowedOrigins = ['http://localhost:3000', 'https://yoursite.com'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,PATCH');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
// Handle preflight
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Max-Age', '86400');
return res.status(204).end();
}
next();
});Django (Python)
# Install django-cors-headers
pip install django-cors-headers
# settings.py
INSTALLED_APPS = [
...
'corsheaders',
...
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # Must be FIRST
'django.middleware.common.CommonMiddleware',
...
]
# Option 1: Allow specific origins
CORS_ALLOWED_ORIGINS = [
'http://localhost:3000',
'https://yoursite.com',
]
# Option 2: Allow all origins (dev only!)
# CORS_ALLOW_ALL_ORIGINS = True
# Allow credentials (cookies, auth headers)
CORS_ALLOW_CREDENTIALS = True
# Allowed headers
CORS_ALLOW_HEADERS = [
'content-type',
'authorization',
'x-requested-with',
]
# Allowed methods
CORS_ALLOW_METHODS = [
'GET',
'POST',
'PUT',
'PATCH',
'DELETE',
'OPTIONS',
]Flask (Python)
# Install flask-cors
pip install flask-cors
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
# Option 1: Allow specific origins
CORS(app, origins=['http://localhost:3000', 'https://yoursite.com'],
supports_credentials=True,
allow_headers=['Content-Type', 'Authorization'],
methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
# Option 2: Manual approach
@app.after_request
def after_request(response):
origin = request.headers.get('Origin')
allowed = ['http://localhost:3000', 'https://yoursite.com']
if origin in allowed:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization'
response.headers['Access-Control-Allow-Credentials'] = 'true'
return responseGo (net/http)
package main
import (
"net/http"
"strings"
)
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
allowedOrigins := map[string]bool{
"http://localhost:3000": true,
"https://yoursite.com": true,
}
origin := r.Header.Get("Origin")
if allowedOrigins[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods",
strings.Join([]string{"GET","POST","PUT","DELETE","PATCH"}, ","))
w.Header().Set("Access-Control-Allow-Headers",
"Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Max-Age", "86400")
}
// Handle preflight
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/data", handleData)
handler := corsMiddleware(mux)
http.ListenAndServe(":8080", handler)
}Configuración Nginx
server {
listen 80;
server_name api.example.com;
# CORS headers for all locations
set $cors_origin "";
if ($http_origin ~* "^https?://(localhost:3000|yoursite\.com)$") {
set $cors_origin $http_origin;
}
location /api/ {
# CORS headers
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400 always;
# Handle preflight requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain';
return 204;
}
proxy_pass http://backend:8080;
}
}Proxy inverso como middleware CORS
Si no puedes modificar la API backend (API de terceros), usa un proxy inverso para añadir cabeceras CORS.
Proxy inverso Nginx
# Nginx as CORS proxy for a third-party API
server {
listen 80;
server_name cors-proxy.yoursite.com;
location /proxy/ {
# Add CORS headers
add_header 'Access-Control-Allow-Origin' 'https://yoursite.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://yoursite.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
# Strip /proxy/ prefix and forward to third-party API
rewrite ^/proxy/(.*) /$1 break;
proxy_pass https://third-party-api.com;
proxy_set_header Host third-party-api.com;
proxy_set_header X-Real-IP $remote_addr;
}
}
# Usage: fetch('https://cors-proxy.yoursite.com/proxy/endpoint')Proxy inverso Apache
# Apache as CORS proxy
<VirtualHost *:80>
ServerName cors-proxy.yoursite.com
# Enable required modules
# a2enmod proxy proxy_http headers rewrite
<Location "/proxy/">
Header always set Access-Control-Allow-Origin "https://yoursite.com"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Content-Type, Authorization"
Header always set Access-Control-Allow-Credentials "true"
# Handle preflight
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]
# Proxy to backend
ProxyPass "http://backend:8080/"
ProxyPassReverse "http://backend:8080/"
</Location>
</VirtualHost>Soluciones solo para desarrollo
Estas soluciones son solo para desarrollo local. Nunca las uses en producción.
Proxy Vite
El proxy integrado de Vite redirige las peticiones del servidor de desarrollo a la API, evitando CORS completamente:
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
// Proxy /api requests to backend
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
// Optional: rewrite path
// rewrite: (path) => path.replace(/^\/api/, ''),
},
// Proxy with WebSocket support
'/ws': {
target: 'ws://localhost:8080',
ws: true,
},
},
},
});
// In your code, use relative URLs:
// fetch('/api/users') instead of fetch('http://localhost:8080/api/users')Proxy Webpack devServer
Create React App y otros setups basados en webpack soportan un proxy similar:
// package.json (Create React App)
{
"proxy": "http://localhost:8080"
}
// Or for more control, create src/setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'http://localhost:8080',
changeOrigin: true,
})
);
};
// webpack.config.js (manual setup)
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
};Rewrites de Next.js
Next.js puede proxificar peticiones API a través de su propio servidor usando rewrites:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'http://localhost:8080/api/:path*',
},
];
},
};
module.exports = nextConfig;
// In your code:
// fetch('/api/users') -- proxied to http://localhost:8080/api/usersTrampas comunes y casos especiales
Trampa 1: Wildcard (*) con credenciales
Cuando el frontend envía credenciales, el servidor NO PUEDE usar *. Debe devolver el origen exacto:
INCORRECTO (fallará con credenciales):
// Frontend
fetch('https://api.example.com/data', {
credentials: 'include' // Sending cookies
});
// Backend response header:
Access-Control-Allow-Origin: * // FAILS! Cannot use * with credentialsCORRECTO (origen dinámico para credenciales):
// Backend (Express.js example)
app.use((req, res, next) => {
// Echo back the exact origin
const origin = req.headers.origin;
const allowedOrigins = ['http://localhost:3000', 'https://yoursite.com'];
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin); // Exact origin
}
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});Trampa 2: Falta manejador OPTIONS (preflight)
Las peticiones no simples disparan un preflight OPTIONS. Si tu servidor devuelve 404 o 405 para OPTIONS, CORS falla.
| Trigger | Example |
|---|---|
| Custom headers | Authorization, X-Request-ID, X-API-Key |
| Non-simple methods | PUT, DELETE, PATCH |
| Non-simple Content-Type | application/json, application/xml |
| ReadableStream body | Streaming request body |
// Ensure your server handles OPTIONS for all API routes:
app.options('*', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,PATCH');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
res.setHeader('Access-Control-Max-Age', '86400');
res.status(204).end();
});Trampa 3: Cookies y SameSite
Las cookies cross-origin requieren tres condiciones:
// 1. Frontend: include credentials
fetch('https://api.example.com/data', {
credentials: 'include'
});
// 2. Backend: allow credentials + exact origin
res.setHeader('Access-Control-Allow-Origin', 'https://yoursite.com'); // NOT *
res.setHeader('Access-Control-Allow-Credentials', 'true');
// 3. Cookie must have correct attributes
Set-Cookie: session=abc123; SameSite=None; Secure; HttpOnly; Path=/
// IMPORTANT: SameSite=None requires Secure (HTTPS).
// This means cross-origin cookies do NOT work on HTTP (localhost).
// For local dev, use a proxy instead of cross-origin cookies.Trampa 4: Cachear respuestas preflight
Las peticiones preflight pueden ser lentas. Usa Access-Control-Max-Age para cachearlas:
// Server response header:
Access-Control-Max-Age: 86400 // Cache preflight for 24 hours
// Without this header, the browser sends a preflight OPTIONS
// request before EVERY non-simple request, adding latency.
// With caching, the browser remembers the CORS permissions
// and skips the preflight for subsequent requests.Nota: Chrome limita a 7200 segundos (2 horas). Firefox a 86400 (24 horas).
Trampa 5: CORS en redirecciones
Si tu endpoint API redirige (301/302), las cabeceras CORS deben estar en AMBAS respuestas. Evita redirecciones en endpoints API.
// PROBLEM: API endpoint redirects
// GET https://api.example.com/users -> 301 -> https://api.example.com/v2/users
// CORS headers must be on BOTH responses!
// SOLUTION: Avoid redirects in API endpoints
// Option 1: Update the frontend URL
fetch('https://api.example.com/v2/users'); // Use final URL directly
// Option 2: Make the redirect include CORS headers
// (requires server configuration on both the old and new endpoint)Referencia rápida: cabeceras CORS
| Cabecera | Propósito | Valor ejemplo |
|---|---|---|
| Access-Control-Allow-Origin | Which origins can access the resource | https://yoursite.com or * |
| Access-Control-Allow-Methods | Which HTTP methods are allowed | GET, POST, PUT, DELETE |
| Access-Control-Allow-Headers | Which request headers are allowed | Content-Type, Authorization |
| Access-Control-Allow-Credentials | Whether cookies/auth can be sent | true |
| Access-Control-Max-Age | How long to cache preflight (seconds) | 86400 |
| Access-Control-Expose-Headers | Which response headers JS can read | X-Total-Count, X-Request-ID |
Preguntas frecuentes
¿Se puede desactivar CORS en el navegador?
Técnicamente sí, lanzando Chrome con --disable-web-security, pero es extremadamente peligroso. Para desarrollo, usa un proxy (Vite, webpack).
¿Por qué mi API funciona en Postman pero no en el navegador?
CORS es un mecanismo de seguridad solo del navegador. Postman, curl y peticiones servidor-a-servidor no aplican CORS.
¿CORS es una corrección de frontend o backend?
Casi siempre backend. El servidor debe enviar las cabeceras CORS correctas. El frontend no puede eludir CORS. La única solución frontend es usar un proxy.
¿Se debe usar Access-Control-Allow-Origin: * en producción?
Solo para APIs verdaderamente públicas sin cookies ni autenticación. Para APIs con credenciales, especifica orígenes exactos.
¿Qué es una petición preflight?
Es una petición OPTIONS automática que el navegador envía antes de la petición real. Ocurre con cabeceras personalizadas, métodos no simples o Content-Type especial.
¿Cómo solucionar CORS para conexiones WebSocket?
Las conexiones WebSocket no siguen las reglas CORS. El servidor debe validar la cabecera Origin manualmente. Si aparece un error CORS, generalmente es el endpoint HTTP inicial.