FastAPI y CORS: La configuración que sí funciona (y por qué)

CORS en FastAPI es un tema que genera muchas dudas. Aquí te explico cómo configurarlo correctamente, qué significa cada opción y por qué la solución fácil no siempre es la mejor.

Tengo que confesarlo: al principio, cada vez que me topaba con un error de CORS en algún proyecto, mi primera reacción era ir a Google, copiar la solución más votada de Stack Overflow que incluyera un comodín (*) y rogar que funcionara. Y sí, la mayoría de las veces funcionaba, pero no entendía el fondo. Solo apagaba el incendio. Esto, por supuesto, me trajo más de un dolor de cabeza más adelante.

CORS, o Cross-Origin Resource Sharing, es un mecanismo de seguridad implementado por los navegadores. No es un capricho del framework o del servidor. Es el navegador el que dice: “Oye, esta aplicación web en dominioA.com está intentando pedir recursos a api.dominioB.com. ¿Está permitido eso?”. Si el servidor en api.dominioB.com no responde explícitamente que sí, el navegador bloquea la petición. Así de simple.

Configurando CORS en FastAPI: Los Fundamentos

FastAPI, al ser un framework moderno y bien pensado, tiene un middleware específico para manejar CORS. Se llama CORSMiddleware y es bastante flexible. Aquí te muestro la configuración básica, que es la que se usa en la mayoría de los casos:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# Orígenes permitidos
# En desarrollo, "*" puede servir, pero NUNCA en producción.
# En producción, especifica tus dominios:
# origins = [
#     "http://localhost:3000", # Para tu frontend local
#     "http://misitio.com",
#     "https://misitio.com",
#     "http://subdominio.misitio.com"
# ]
origins = [
    "http://localhost:3000", # Ejemplo para un frontend React/Vue/Angular local
    "http://127.0.0.1:3000",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=["*"] # O especifica: ["Authorization", "Content-Type", "X-Custom-Header"]
)

@app.get("/")
async def read_root():
    return {"message": "Hello from FastAPI!"}

@app.post("/items/")
async def create_item(item: dict):
    return {"item": item, "message": "Item created"}

# Para ejecutar:
# uvicorn main:app --reload --host 0.0.0.0 --port 8000

Vamos a desglosar cada parámetro, porque es clave entenderlos para no caer en la sobreingeniería o, peor aún, en agujeros de seguridad:

  • allow_origins: Esta es la parte más importante. Aquí le dices a tu API qué dominios (orígenes) están permitidos para hacer solicitudes.
    • Si pones ["*" ] (como en la primera respuesta de Stack Overflow), estás diciendo: “Cualquier dominio puede acceder a mi API”. Esto es práctico para prototipos o desarrollo muy temprano, pero es una invitación a problemas de seguridad en producción. Estás desactivando la protección que CORS te ofrece. Yo no lo recomiendo para nada en entornos reales, por más que veas que mucha gente lo usa.
    • Lo correcto es listar explícitamente los dominios que van a interactuar con tu API. Por ejemplo, si tienes un frontend en mi-frontend.com y una API en mi-api.com, en allow_origins de tu API deberías poner ["https://mi-frontend.com"]. Si tu frontend usa varios subdominios o puertos diferentes en desarrollo, tendrás que listarlos todos.
  • allow_credentials: Si tu aplicación necesita enviar cookies, cabeceras de autorización HTTP o certificados TLS con las solicitudes de origen cruzado, debes establecer esto en True. Esto es común en sistemas de autenticación. Ojo que si lo pones en True, allow_origins no puede ser ["*" ]. Tienes que especificar los orígenes exactos, por la misma razón de seguridad.
  • allow_methods: Aquí defines qué métodos HTTP (GET, POST, PUT, DELETE, etc.) están permitidos para las solicitudes de origen cruzado. Poner ["*" ] es común y, la verdad, no tiene un impacto de seguridad tan grande como en allow_origins, ya que los métodos que tu API realmente soporte los definirá tu código de FastAPI. Sin embargo, si quieres ser estricto, puedes listar solo los que necesites, como en mi ejemplo.
  • allow_headers: Similar a los métodos, esto permite especificar qué cabeceras HTTP personalizadas están permitidas. Si tu frontend envía cabeceras como Authorization, Content-Type o cualquier otra cabecera custom (por ejemplo, X-Mi-Cabecera-Custom), tienes que listarlas aquí. Poner ["*" ] es también bastante común y, en general, está bien, a menos que tengas requisitos de seguridad muy específicos para las cabeceras.

Cuidado con los errores que parecen CORS, pero no lo son

La segunda respuesta de Stack Overflow toca un punto crucial que me ha costado horas de debugging en el pasado: a veces, el problema no es CORS, sino un error en tu propio backend. El navegador te dirá que la solicitud fue bloqueada por CORS, pero la causa raíz es que tu servidor respondió con un 500 (Error Interno del Servidor) o algo inesperado antes de que pudiera aplicar correctamente las cabeceras CORS. Me frustra cuando la gente se enfoca solo en CORS cuando el verdadero problema está en su código.

Si estás seguro de que tu configuración CORS es correcta (es decir, el allow_origins está bien y el resto de los parámetros también), y sigues viendo errores en el cliente, revisa los logs de tu servidor FastAPI primero. Busca cualquier excepción, error de base de datos, problema de validación de Pydantic o cualquier otra cosa que pueda estar haciendo que tu endpoint falle antes de enviar una respuesta válida. Si el servidor devuelve un error, el navegador no recibirá las cabeceras CORS esperadas y lo reportará como un problema de origen cruzado, incluso si el middleware CORS de FastAPI está configurado correctamente.

De hecho, este es un patrón común: la aplicación frontend hace una petición, el backend falla internamente, el navegador no ve las cabeceras CORS correctas en la respuesta de error y ¡pum! CORS error en consola. Es una de esas cosas que aprendes a la mala, lamentablemente.

Mi filosofía y qué haría diferente si empezara de cero

Mi enfoque con CORS es siempre ir a lo mínimo necesario. No me gusta la idea de abrir las puertas de mi API a todo el mundo si no es estrictamente necesario. Es una cuestión de seguridad básica. Prefiero que la gente me pida acceso explícitamente si lo necesita.

Si tuviera que empezar de cero un proyecto hoy, lo primero que haría sería definir claramente qué orígenes van a consumir mi API. Instalaría el CORSMiddleware desde el día uno, pero en vez de usar el comodín *, pondría ["http://localhost:3000"] para desarrollo y, tan pronto como tuviera un entorno de staging o producción, lo cambiaría a los dominios específicos. No esperaría a que me apareciera el error en el navegador para ir a buscar la solución parche. Y, más importante aún, me aseguraría de que mi código de backend sea robusto para evitar que errores internos se disfracen de problemas de CORS. Eso te ahorra muchísimos dolores de cabeza a largo plazo, créeme.

Jorge RequenaDeveloper full-stack · Chile