Errores CORS en API Gateway: Cómo habilitarlo y qué cabeceras debe devolver tu Lambda

Tu frontend hace una petición a API Gateway y el navegador la bloquea con un error CORS antes de que llegue ninguna respuesta útil. El problema casi nunca está en un solo lugar: API Gateway necesita responder al preflight OPTIONS, y tu función Lambda debe incluir las cabeceras correctas en cada respuesta. Si falla cualquiera de los dos lados, el navegador rechaza la llamada igualmente.

TL;DR — Resumen rápido

CapaQué debes configurarDónde
API Gateway (REST)Habilitar CORS en el recurso, desplegar el stageConsola / CLI / SAM
API Gateway (HTTP)Configurar CORS en la sección 'CORS' de la APIConsola / CLI
Lambda (ambos tipos)Incluir Access-Control-Allow-Origin y cabeceras relacionadas en la respuestaCódigo de la función
Preflight OPTIONSDebe responder con 200 y las cabeceras correctasAPI Gateway o Lambda

Cómo funciona CORS en API Gateway

CORS (Cross-Origin Resource Sharing) es un mecanismo del navegador, no de AWS. Cuando tu frontend en https://app.ejemplo.com llama a https://api.ejemplo.com, el navegador detecta que los orígenes son distintos y envía primero una petición preflight con método OPTIONS. Si el servidor no responde con las cabeceras CORS correctas, el navegador bloquea la petición real antes de enviarla.

API Gateway tiene dos tipos de API con comportamientos distintos en CORS:

  • REST API (v1): Cuando activas 'Enable CORS' en la consola, API Gateway crea automáticamente un método OPTIONS con una respuesta mock que incluye las cabeceras necesarias. Sin embargo, las respuestas de tus métodos reales (GET, POST, etc.) deben incluir las cabeceras CORS desde tu Lambda, porque API Gateway REST no las inyecta automáticamente en respuestas de integración Lambda.
  • HTTP API (v2): La configuración CORS se define a nivel de API y API Gateway gestiona las respuestas preflight automáticamente. Aun así, si usas respuestas personalizadas desde Lambda, debes verificar que no sobreescriben las cabeceras.
sequenceDiagram participant Nav as Navegador participant APIGW as API Gateway participant Fn as Lambda Nav->>APIGW: OPTIONS /usuarios
(preflight) APIGW-->>Nav: 200 + Access-Control-Allow-*
(respuesta mock) Nav->>APIGW: POST /usuarios
(petición real) APIGW->>Fn: Invoca Lambda Fn-->>APIGW: 200 + cabeceras CORS + body APIGW-->>Nav: 200 + cabeceras CORS + body Note over Nav: Navegador acepta la respuesta
  1. Petición preflight OPTIONS: El navegador la envía antes de la petición real cuando detecta un origen cruzado.
  2. API Gateway responde al OPTIONS: En REST API, con el método mock configurado. En HTTP API, automáticamente si CORS está habilitado.
  3. Petición real (GET/POST): Solo se envía si el preflight fue exitoso.
  4. Lambda devuelve la respuesta: Debe incluir Access-Control-Allow-Origin en sus cabeceras, o el navegador rechaza la respuesta aunque llegue con código 200.

Paso 1 — Identificar el tipo de API Gateway que estás usando

Antes de tocar cualquier configuración, confirma si tu API es REST (v1) o HTTP (v2). El comportamiento de CORS es fundamentalmente diferente entre ambas y los pasos de configuración no son intercambiables.

# Lista todas tus APIs y su tipo (REST o HTTP)
aws apigateway get-rest-apis \
  --query 'items[*].{Nombre:name,ID:id}' \
  --output table

# Para HTTP APIs (API Gateway v2)
aws apigatewayv2 get-apis \
  --query 'Items[*].{Nombre:Name,ID:ApiId,Tipo:ProtocolType}' \
  --output table

Si tu API aparece en el primer comando, es una REST API. Si aparece en el segundo con ProtocolType: HTTP, es una HTTP API. Los pasos siguientes están separados por tipo.

Paso 2A — Habilitar CORS en una REST API (v1)

La consola de API Gateway tiene un botón 'Enable CORS' que genera el método OPTIONS con una respuesta mock. Lo que no hace es añadir cabeceras a tus respuestas Lambda — eso sigue siendo tu responsabilidad.

Desde la consola

  1. Abre la consola de API Gateway y selecciona tu REST API.
  2. En el panel de recursos, selecciona el recurso (por ejemplo, /usuarios).
  3. Haz clic en Actions → Enable CORS.
  4. Configura los valores: Access-Control-Allow-Origin (usa el origen exacto de tu frontend, no * en producción si envías cookies o cabeceras de autorización), Access-Control-Allow-Headers, y Access-Control-Allow-Methods.
  5. Haz clic en Enable CORS and replace existing CORS headers.
  6. Crítico: Despliega la API. Sin un nuevo despliegue, los cambios no tienen efecto. Ve a Actions → Deploy API y selecciona tu stage.

Desde la CLI

La CLI no tiene un comando único equivalente al botón de la consola. Debes crear el método OPTIONS, su integración mock, y las respuestas de método con las cabeceras. El siguiente bloque muestra la secuencia completa para un recurso existente.

🔽 Ver comandos CLI completos para configurar OPTIONS en REST API
# Variables — ajusta estos valores
API_ID='abc123def'
RESOURCE_ID='xyz789'
STAGE_NAME='prod'
REGION='us-east-1'

# 1. Crear el método OPTIONS
aws apigateway put-method \
  --rest-api-id $API_ID \
  --resource-id $RESOURCE_ID \
  --http-method OPTIONS \
  --authorization-type NONE \
  --region $REGION

# 2. Crear la integración mock
aws apigateway put-integration \
  --rest-api-id $API_ID \
  --resource-id $RESOURCE_ID \
  --http-method OPTIONS \
  --type MOCK \
  --request-templates '{"application/json": "{\"statusCode\": 200}"}' \
  --region $REGION

# 3. Crear la respuesta del método con código 200
aws apigateway put-method-response \
  --rest-api-id $API_ID \
  --resource-id $RESOURCE_ID \
  --http-method OPTIONS \
  --status-code 200 \
  --response-parameters '{"method.response.header.Access-Control-Allow-Headers": false, "method.response.header.Access-Control-Allow-Methods": false, "method.response.header.Access-Control-Allow-Origin": false}' \
  --region $REGION

# 4. Crear la respuesta de integración con los valores de cabecera
aws apigateway put-integration-response \
  --rest-api-id $API_ID \
  --resource-id $RESOURCE_ID \
  --http-method OPTIONS \
  --status-code 200 \
  --response-parameters '{"method.response.header.Access-Control-Allow-Headers": "\"Content-Type,X-Amz-Date,Authorization,X-Api-Key\"", "method.response.header.Access-Control-Allow-Methods": "\"GET,POST,OPTIONS\"", "method.response.header.Access-Control-Allow-Origin": "\"https://app.ejemplo.com\""}' \
  --region $REGION

# 5. Desplegar — sin este paso nada funciona
aws apigateway create-deployment \
  --rest-api-id $API_ID \
  --stage-name $STAGE_NAME \
  --region $REGION

Paso 2B — Habilitar CORS en una HTTP API (v2)

Las HTTP APIs tienen una configuración CORS centralizada que API Gateway aplica automáticamente a las respuestas preflight. Es considerablemente más simple que el enfoque REST.

# Configurar CORS en una HTTP API existente
aws apigatewayv2 update-api \
  --api-id 'abc123def' \
  --cors-configuration \
    AllowOrigins='https://app.ejemplo.com',AllowMethods='GET,POST,OPTIONS',AllowHeaders='Content-Type,Authorization',MaxAge=300 \
  --region us-east-1

Verifica que la configuración se aplicó correctamente:

aws apigatewayv2 get-api \
  --api-id 'abc123def' \
  --query 'CorsConfiguration' \
  --region us-east-1

Paso 3 — Cabeceras que tu Lambda debe incluir en cada respuesta

Aquí está el error más frecuente: los ingenieros configuran CORS en API Gateway, prueban el preflight OPTIONS y funciona, pero la petición real sigue fallando. El motivo es que Lambda devuelve la respuesta sin las cabeceras CORS, y el navegador la rechaza igualmente.

En una REST API con integración Lambda proxy, API Gateway pasa la respuesta de Lambda directamente al cliente sin modificar las cabeceras. Tu función debe incluirlas explícitamente.

Ejemplo en Python (Lambda proxy integration)

import json

def lambda_handler(event, context):
    # Tu lógica de negocio aquí
    resultado = {'mensaje': 'Operación exitosa'}

    return {
        'statusCode': 200,
        'headers': {
            'Content-Type': 'application/json',
            # Usa el origen exacto de tu frontend, no '*' si envías
            # cabeceras Authorization o cookies (credentials: true)
            'Access-Control-Allow-Origin': 'https://app.ejemplo.com',
            'Access-Control-Allow-Headers': 'Content-Type,Authorization',
            'Access-Control-Allow-Methods': 'GET,POST,OPTIONS'
        },
        'body': json.dumps(resultado)
    }

Ejemplo en Node.js

exports.handler = async (event) => {
    const resultado = { mensaje: 'Operación exitosa' };

    return {
        statusCode: 200,
        headers: {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': 'https://app.ejemplo.com',
            'Access-Control-Allow-Headers': 'Content-Type,Authorization',
            'Access-Control-Allow-Methods': 'GET,POST,OPTIONS'
        },
        body: JSON.stringify(resultado)
    };
};
CORS es como el portero de un club: el preflight OPTIONS es la lista de invitados, pero cada respuesta real también necesita el sello de entrada. Si tu Lambda no devuelve Access-Control-Allow-Origin, el navegador tira la respuesta aunque el contenido sea perfecto.

Paso 4 — Verificar el preflight desde la terminal

Antes de depurar desde el navegador, confirma el comportamiento del preflight directamente con curl. Esto elimina variables del navegador y te da la respuesta exacta que devuelve API Gateway.

# Simular una petición preflight OPTIONS
curl -v -X OPTIONS \
  'https://abc123def.execute-api.us-east-1.amazonaws.com/prod/usuarios' \
  -H 'Origin: https://app.ejemplo.com' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: Content-Type,Authorization'

La respuesta debe incluir código 200 y las cabeceras Access-Control-Allow-Origin, Access-Control-Allow-Methods, y Access-Control-Allow-Headers. Si ves un 403 o las cabeceras están ausentes, el problema está en la configuración de API Gateway, no en Lambda.

# Verificar la respuesta real (GET/POST) con las cabeceras CORS
curl -v -X GET \
  'https://abc123def.execute-api.us-east-1.amazonaws.com/prod/usuarios' \
  -H 'Origin: https://app.ejemplo.com' \
  -H 'Authorization: Bearer tu-token-aqui'

El diagnóstico que nadie hace hasta que ya perdió una hora

El escenario clásico: configuras CORS en la consola, despliegas, pruebas desde el navegador y el error persiste. Revisas la configuración tres veces. Todo parece correcto. El preflight OPTIONS devuelve 200 con las cabeceras correctas. Pero la petición POST sigue fallando con 'No Access-Control-Allow-Origin header present'.

El diagnóstico incorrecto: 'API Gateway no está aplicando la configuración CORS'. Pasas tiempo revisando la consola, borrando y recreando el método OPTIONS.

La causa real: tu Lambda está lanzando una excepción no controlada. Cuando Lambda falla con un error no capturado en una integración proxy, API Gateway devuelve un 502 con un cuerpo de error genérico — y sin las cabeceras CORS, porque esas cabeceras las pone tu código Lambda, que nunca llegó a ejecutarse completamente.

El navegador ve un 502 sin Access-Control-Allow-Origin y reporta un error CORS, ocultando el error real de la aplicación.

# Revisa los logs de Lambda para encontrar el error real
aws logs filter-log-events \
  --log-group-name '/aws/lambda/nombre-de-tu-funcion' \
  --filter-pattern 'ERROR' \
  --start-time $(date -d '1 hour ago' +%s000) \
  --region us-east-1 \
  --query 'events[*].message' \
  --output text

Siempre revisa los logs de Lambda antes de asumir que el problema es de configuración CORS.

Consideraciones de seguridad: por qué no usar * en producción

Usar Access-Control-Allow-Origin: * es tentador porque simplifica la configuración, pero tiene implicaciones importantes:

  • Si tu frontend envía la cabecera Authorization o usa credentials: true en el fetch, el navegador rechazará la respuesta si el origen es *. La especificación CORS prohíbe explícitamente usar credenciales con origen wildcard.
  • En producción, especifica los orígenes permitidos explícitamente. Si necesitas múltiples orígenes, valida el origen en tu Lambda y devuelve el valor correspondiente en Access-Control-Allow-Origin.
import json

ORIGENES_PERMITIDOS = [
    'https://app.ejemplo.com',
    'https://staging.ejemplo.com'
]

def lambda_handler(event, context):
    origen_solicitado = event.get('headers', {}).get('origin', '')

    # Solo reflejar el origen si está en la lista permitida
    origen_respuesta = origen_solicitado if origen_solicitado in ORIGENES_PERMITIDOS else ''

    return {
        'statusCode': 200,
        'headers': {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': origen_respuesta,
            'Access-Control-Allow-Headers': 'Content-Type,Authorization',
            'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
            'Vary': 'Origin'
        },
        'body': json.dumps({'mensaje': 'OK'})
    }

La cabecera Vary: Origin es necesaria cuando el valor de Access-Control-Allow-Origin varía según la petición, para que los proxies y cachés intermedios no sirvan una respuesta con el origen incorrecto a otro cliente.

Diagnóstico rápido: árbol de decisión CORS

graph TD A[Error CORS en el navegador] --> B{Preflight OPTIONS
devuelve 200?} B -- No --> C[Problema en API Gateway
Revisar método OPTIONS
y despliegue del stage] B -- Si --> D{Cabeceras CORS
en respuesta OPTIONS?} D -- No --> E[Revisar integración mock
y response parameters] D -- Si --> F{Cabeceras CORS
en respuesta real?} F -- No --> G[Problema en Lambda
Añadir cabeceras en el return] F -- Si --> H{Lambda devuelve
error o 5xx?} H -- Si --> I[Revisar CloudWatch Logs
Error de app enmascarado] H -- No --> J[Verificar origen exacto
y uso de credentials]
  1. ¿El preflight OPTIONS devuelve 200? Si no, el problema está en la configuración de API Gateway — método OPTIONS ausente o no desplegado.
  2. ¿Las cabeceras CORS están en la respuesta OPTIONS? Si no, revisa la configuración del método OPTIONS y su integración mock.
  3. ¿La petición real devuelve las cabeceras CORS? Si no, el problema está en el código Lambda.
  4. ¿Lambda está devolviendo un error? Revisa CloudWatch Logs — un error de aplicación puede enmascararse como error CORS.

Permisos IAM necesarios para configurar CORS

Si estás configurando CORS desde la CLI o automatizando con IaC, tu rol o usuario necesita los permisos correspondientes sobre API Gateway.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ConfigurarCORSRestAPI",
      "Effect": "Allow",
      "Action": [
        "apigateway:GET",
        "apigateway:PUT",
        "apigateway:POST",
        "apigateway:DELETE",
        "apigateway:PATCH"
      ],
      "Resource": [
        "arn:aws:apigateway:us-east-1::/restapis/abc123def/*"
      ]
    },
    {
      "Sid": "ConfigurarCORSHttpAPI",
      "Effect": "Allow",
      "Action": [
        "apigateway:GET",
        "apigateway:PATCH"
      ],
      "Resource": [
        "arn:aws:apigateway:us-east-1::/apis/abc123def"
      ]
    }
  ]
}

Conclusión y próximos pasos para resolver errores CORS en API Gateway

Los errores CORS en API Gateway tienen dos capas que deben funcionar juntas: API Gateway debe responder correctamente al preflight OPTIONS, y tu Lambda debe incluir las cabeceras CORS en cada respuesta real. Falla una sola capa y el navegador bloquea la llamada.

Si después de seguir estos pasos el error persiste, el siguiente punto de revisión es CloudWatch Logs de Lambda — un error de aplicación que devuelve 502 se manifiesta como error CORS en el navegador porque Lambda nunca llega a ejecutar el código que añade las cabeceras.

Glosario de términos clave

TérminoDefinición
CORSCross-Origin Resource Sharing. Mecanismo del navegador que controla qué orígenes pueden acceder a recursos de otro origen.
PreflightPetición HTTP OPTIONS que el navegador envía automáticamente antes de una petición cross-origin para verificar que el servidor la permite.
Lambda Proxy IntegrationModo de integración donde API Gateway pasa el evento completo a Lambda y devuelve su respuesta directamente al cliente sin transformaciones.
REST API (v1)Tipo de API Gateway con configuración por recurso y método. Requiere despliegue explícito para aplicar cambios.
HTTP API (v2)Tipo de API Gateway más reciente con configuración CORS centralizada y menor latencia que las REST APIs.

Comentarios

Entradas populares de este blog

EC2 sin acceso a Internet en VPC personalizada: Internet Gateway y Route Table

Actualizar Contenido en CloudFront: Cómo Crear una Invalidación para Limpiar el Caché del Edge

Aumentar el Timeout de Lambda: Configuración, Límites y Diagnóstico en Producción