Módulo 3: Serverless & Lambda for AI

6. API Gateway Integration

Descripción

En esta cápsula vas a conectar tu Lambda a internet a través de API Gateway. Una Lambda sin trigger HTTP es una función que nadie puede llamar — API Gateway es lo que convierte tu función en un endpoint real. Al terminar, tendrás un endpoint POST que recibe un prompt, invoca tu Lambda AI, y retorna la respuesta con CORS configurado, autenticación básica, y rate limiting.

Contexto: API Gateway es el servicio AWS que expone tus Lambdas como endpoints HTTP. Hay dos versiones: REST API (v1) y HTTP API (v2). Para AI endpoints, HTTP API es casi siempre la opción correcta — más barato, más simple, y con mejor rendimiento. Esta cápsula cubre ambas para que entiendas cuándo usar cada una, pero el proyecto usa HTTP API.


API Gateway como Trigger HTTP

El flujo completo

Cliente (browser, app, curl)
    │
    │  POST /ask  {"prompt": "..."}
    ▼
┌──────────────┐
│ API Gateway  │  ← Recibe HTTP, valida, transforma
│  (HTTP API)  │
└──────┬───────┘
       │  Invoca Lambda (síncrono)
       ▼
┌──────────────┐
│   Lambda     │  ← Ejecuta handler, llama a OpenAI
│ (ai-endpoint)│
└──────┬───────┘
       │  Retorna response
       ▼
┌──────────────┐
│ API Gateway  │  ← Transforma response, agrega headers
└──────┬───────┘
       │  HTTP 200 {"answer": "..."}
       ▼
Cliente recibe respuesta

Latencia del flujo

Componente          Latencia típica
────────────────────────────────────
API Gateway         5-15ms
Lambda cold start   1-8s (primera vez)
Lambda warm         <50ms
LLM API call        1-20s (según modelo/tokens)
API Gateway return  3-10ms
────────────────────────────────────
Total (warm):       1.5-20.5s
Total (cold):       2.5-28s

REST API vs HTTP API (v2)

Comparación directa

Feature                REST API (v1)      HTTP API (v2)
──────────────────────────────────────────────────────────
Precio (por millón)    $3.50              $1.00
Latencia overhead      15-30ms            5-10ms
WebSocket support      ✅                  ✅
Request validation     ✅ (built-in)       ❌ (en tu código)
Usage plans/API keys   ✅ (nativo)         ❌ (manual)
Custom authorizers     ✅ (Lambda + IAM)   ✅ (Lambda + JWT)
Request/response map   ✅ (VTL templates)  ❌
WAF integration        ✅                  ❌
Caching                ✅ (built-in)       ❌
CORS                   ✅ (manual config)  ✅ (simple config)
Payload max            10 MB              10 MB
Timeout max            29 seconds         29 seconds ⚠️

Cuándo usar cada una

Usa HTTP API (v2) cuando:
├── Tu AI endpoint es simple: recibe prompt → retorna response
├── No necesitas caching en Gateway (lo haces en Lambda/Redis)
├── Quieres minimizar costes (3.5x más barato)
├── La latencia importa (menos overhead)
└── 90% de los casos de esta guía

Usa REST API (v1) cuando:
├── Necesitas API keys nativas con usage plans
├── Necesitas request validation en Gateway (antes de invocar Lambda)
├── Necesitas WAF (Web Application Firewall) por compliance
├── Necesitas caching en Gateway para reducir invocaciones Lambda
└── Enterprise con controles de acceso granulares

El límite de 29 segundos

⚠️ Ambas versiones de API Gateway tienen un timeout máximo de 29 segundos. Esto es un hard limit que no puedes cambiar. Tu Lambda puede tener timeout de 15 minutos, pero si la invocas via API Gateway, debe responder en menos de 29 segundos.

Implicaciones para AI:
├── Llamadas simples a gpt-4o-mini (1-3s): ✅ sin problema
├── Llamadas a gpt-4o con muchos tokens (5-15s): ✅ generalmente OK
├── Prompt chains (3+ llamadas secuenciales): ⚠️ arriesgado
├── RAG pipeline complejo: ⚠️ puede exceder 29s
└── Batch processing: ❌ no uses API Gateway, usa invocación directa

Para workloads >29s:
├── Patrón async: API Gateway inicia Lambda → retorna requestId
│   Lambda procesa en background → resultado en S3/DynamoDB
│   Cliente poll con GET /status/{requestId}
└── Invocación directa: aws lambda invoke (sin Gateway)

Configurar HTTP API (v2)

Con SAM template

# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Runtime: python3.11
    Architectures: [arm64]

Resources:
  AiApi:
    Type: AWS::Serverless::HttpApi
    Properties:
      StageName: prod
      CorsConfiguration:
        AllowOrigins:
          - "https://tuapp.com"
          - "http://localhost:3000"
        AllowMethods:
          - POST
          - GET
          - OPTIONS
        AllowHeaders:
          - Content-Type
          - Authorization
          - X-Api-Key
        MaxAge: 3600

  AskFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: handler.handler
      CodeUri: ./src/
      MemorySize: 768
      Timeout: 60
      Environment:
        Variables:
          OPENAI_API_KEY: !Ref OpenAiApiKey
          MODEL_NAME: gpt-4o-mini
      Events:
        AskRoute:
          Type: HttpApi
          Properties:
            ApiId: !Ref AiApi
            Path: /ask
            Method: POST
        HealthRoute:
          Type: HttpApi
          Properties:
            ApiId: !Ref AiApi
            Path: /health
            Method: GET

  HealthFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: health.handler
      CodeUri: ./src/
      MemorySize: 128
      Timeout: 5
      Events:
        HealthRoute:
          Type: HttpApi
          Properties:
            ApiId: !Ref AiApi
            Path: /health
            Method: GET

Parameters:
  OpenAiApiKey:
    Type: String
    NoEcho: true

Outputs:
  ApiUrl:
    Description: URL del API Gateway
    Value: !Sub "https://${AiApi}.execute-api.${AWS::Region}.amazonaws.com/prod"

Con AWS CLI

# Crear HTTP API
aws apigatewayv2 create-api \
  --name ai-endpoint-api \
  --protocol-type HTTP \
  --cors-configuration '{
    "AllowOrigins": ["http://localhost:3000"],
    "AllowMethods": ["POST", "GET", "OPTIONS"],
    "AllowHeaders": ["Content-Type", "Authorization"],
    "MaxAge": 3600
  }'

# Crear integración con Lambda
aws apigatewayv2 create-integration \
  --api-id API_ID \
  --integration-type AWS_PROXY \
  --integration-uri arn:aws:lambda:us-east-1:ACCOUNT:function:ai-endpoint \
  --payload-format-version 2.0

# Crear ruta POST /ask
aws apigatewayv2 create-route \
  --api-id API_ID \
  --route-key "POST /ask" \
  --target "integrations/INTEGRATION_ID"

# Crear stage y deploy
aws apigatewayv2 create-stage \
  --api-id API_ID \
  --stage-name prod \
  --auto-deploy

CORS: Por qué tu frontend recibe errores

El problema

Tu frontend (localhost:3000):
  fetch("https://xyz.execute-api.us-east-1.amazonaws.com/prod/ask", {
    method: "POST",
    body: JSON.stringify({prompt: "Hola"})
  })

Sin CORS configurado:
  → Browser envía preflight OPTIONS request
  → API Gateway no sabe qué responder
  → Browser bloquea la respuesta
  → Console: "Access to fetch has been blocked by CORS policy"

Configuración CORS en HTTP API

# En SAM template (la forma más simple)
AiApi:
  Type: AWS::Serverless::HttpApi
  Properties:
    CorsConfiguration:
      AllowOrigins:
        - "http://localhost:3000"
        - "https://tuapp.com"
      AllowMethods:
        - POST
        - GET
        - OPTIONS
      AllowHeaders:
        - Content-Type
        - Authorization
      MaxAge: 3600  # Browser cachea preflight por 1 hora

CORS headers en tu Lambda

HTTP API maneja CORS automáticamente si lo configuras en el template. Pero si usas REST API o quieres control explícito:

def handler(event, context):
    headers = {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": os.environ.get("ALLOWED_ORIGIN", "*"),
        "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
        "Access-Control-Allow-Headers": "Content-Type, Authorization",
    }

    # Preflight request
    if event.get("requestContext", {}).get("http", {}).get("method") == "OPTIONS":
        return {"statusCode": 200, "headers": headers, "body": ""}

    # Tu lógica normal
    body = json.loads(event.get("body", "{}"))
    # ... LLM call ...

    return {
        "statusCode": 200,
        "headers": headers,
        "body": json.dumps({"answer": "..."})
    }

Debugging CORS

# Simular preflight desde terminal
curl -v -X OPTIONS \
  https://xyz.execute-api.us-east-1.amazonaws.com/prod/ask \
  -H "Origin: http://localhost:3000" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type"

# Debes ver en la respuesta:
# Access-Control-Allow-Origin: http://localhost:3000
# Access-Control-Allow-Methods: POST, GET, OPTIONS
# Access-Control-Allow-Headers: Content-Type

Request/Response en API Gateway v2

Formato del evento Lambda (payload v2.0)

# Lo que tu Lambda recibe de HTTP API (payload format 2.0):
event = {
    "version": "2.0",
    "routeKey": "POST /ask",
    "rawPath": "/prod/ask",
    "headers": {
        "content-type": "application/json",
        "authorization": "Bearer sk-...",
        "x-forwarded-for": "203.0.113.1",
    },
    "requestContext": {
        "http": {
            "method": "POST",
            "path": "/prod/ask",
            "sourceIp": "203.0.113.1",
        },
        "time": "08/Mar/2026:12:00:00 +0000",
        "requestId": "abc123",
    },
    "body": "{\"prompt\": \"¿Qué es serverless?\", \"max_tokens\": 500}",
    "isBase64Encoded": False,
}

Handler que parsea correctamente

import json
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"], timeout=25)

CORS_HEADERS = {
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin": os.environ.get("ALLOWED_ORIGIN", "*"),
}

def handler(event, context):
    method = event.get("requestContext", {}).get("http", {}).get("method", "")

    if method == "OPTIONS":
        return {"statusCode": 200, "headers": CORS_HEADERS, "body": ""}

    try:
        body = json.loads(event.get("body", "{}"))
    except json.JSONDecodeError:
        return {
            "statusCode": 400,
            "headers": CORS_HEADERS,
            "body": json.dumps({"error": "Invalid JSON body"})
        }

    prompt = body.get("prompt", "").strip()
    if not prompt:
        return {
            "statusCode": 400,
            "headers": CORS_HEADERS,
            "body": json.dumps({"error": "prompt is required"})
        }

    max_tokens = min(body.get("max_tokens", 500), 2000)

    try:
        response = client.chat.completions.create(
            model=os.environ.get("MODEL_NAME", "gpt-4o-mini"),
            messages=[
                {"role": "system", "content": "Responde de forma concisa y útil."},
                {"role": "user", "content": prompt}
            ],
            max_tokens=max_tokens,
        )

        return {
            "statusCode": 200,
            "headers": CORS_HEADERS,
            "body": json.dumps({
                "answer": response.choices[0].message.content,
                "model": response.model,
                "tokens_used": response.usage.total_tokens,
            })
        }

    except Exception as e:
        return {
            "statusCode": 502,
            "headers": CORS_HEADERS,
            "body": json.dumps({"error": f"LLM call failed: {str(e)}"})
        }

Response format

# Lo que tu Lambda debe retornar:
{
    "statusCode": 200,
    "headers": {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": "http://localhost:3000"
    },
    "body": "{\"answer\": \"Serverless es...\", \"tokens_used\": 42}"
}

# API Gateway toma esto y construye la HTTP response para el cliente.
# El body DEBE ser string (JSON serializado), no dict.

Autenticación

Opción 1: API Key (simple)

Para proteger tu endpoint sin infraestructura compleja:

import os
import json

VALID_API_KEYS = set(os.environ.get("API_KEYS", "").split(","))

def handler(event, context):
    api_key = event.get("headers", {}).get("x-api-key", "")

    if api_key not in VALID_API_KEYS:
        return {
            "statusCode": 401,
            "body": json.dumps({"error": "Invalid or missing API key"})
        }

    # ... resto del handler
# Invocar con API key
curl -X POST https://xyz.execute-api.us-east-1.amazonaws.com/prod/ask \
  -H "Content-Type: application/json" \
  -H "x-api-key: tu-api-key-aqui" \
  -d '{"prompt": "Hola"}'

Opción 2: JWT Authorizer (HTTP API v2)

# SAM template con JWT authorizer
Resources:
  AiApi:
    Type: AWS::Serverless::HttpApi
    Properties:
      Auth:
        DefaultAuthorizer: JwtAuthorizer
        Authorizers:
          JwtAuthorizer:
            AuthorizationScopes:
              - ai.invoke
            IdentitySource: "$request.header.Authorization"
            JwtConfiguration:
              issuer: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_POOLID"
              audience:
                - "your-client-id"

  AskFunction:
    Type: AWS::Serverless::Function
    Properties:
      Events:
        AskRoute:
          Type: HttpApi
          Properties:
            ApiId: !Ref AiApi
            Path: /ask
            Method: POST
            # El JWT authorizer se aplica automáticamente
        HealthRoute:
          Type: HttpApi
          Properties:
            ApiId: !Ref AiApi
            Path: /health
            Method: GET
            Auth:
              Authorizer: NONE  # Health check sin auth

Opción 3: IAM Authorization

# Invocar con AWS credentials (service-to-service)
aws lambda invoke \
  --function-name ai-endpoint \
  --payload '{"body": "{\"prompt\": \"Hola\"}"}' \
  response.json

# O con Signature V4 para HTTP
# Útil cuando otro servicio AWS invoca tu endpoint

Recomendación para esta guía

Para desarrollo y learning:
└── API key en header (Opción 1) — simple, funcional

Para producción:
├── JWT con Cognito/Auth0 (Opción 2) — si tienes usuarios
└── IAM (Opción 3) — si es service-to-service

Rate Limiting y Throttling

Throttling en HTTP API

HTTP API v2 defaults:
├── Account-level: 10,000 requests/second
├── Route-level: configurable
└── Burst: 5,000 concurrent

Para AI endpoints, 10K/s es más que suficiente.
Tu cuello de botella es OpenAI rate limits, no API Gateway.

Configurar throttling por ruta

# SAM template
Resources:
  AiApi:
    Type: AWS::Serverless::HttpApi
    Properties:
      StageName: prod
      RouteSettings:
        "POST /ask":
          ThrottlingBurstLimit: 50    # Máximo concurrent
          ThrottlingRateLimit: 100    # Requests por segundo
        "GET /health":
          ThrottlingBurstLimit: 200
          ThrottlingRateLimit: 500

Rate limiting en tu Lambda

API Gateway throttling protege tu Lambda, pero no protege tu cuenta de OpenAI. Implementa rate limiting propio:

import json
import os
import time
import hashlib
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"], timeout=25)

MAX_REQUESTS_PER_MINUTE = int(os.environ.get("RATE_LIMIT", "30"))

# En producción, usa Redis/DynamoDB para rate limiting distribuido
# Esto es una versión simplificada para una sola instancia Lambda
request_log = {}

def check_rate_limit(client_ip):
    now = time.time()
    window_start = now - 60

    if client_ip not in request_log:
        request_log[client_ip] = []

    request_log[client_ip] = [
        t for t in request_log[client_ip] if t > window_start
    ]

    if len(request_log[client_ip]) >= MAX_REQUESTS_PER_MINUTE:
        return False

    request_log[client_ip].append(now)
    return True

def handler(event, context):
    client_ip = (
        event.get("requestContext", {})
        .get("http", {})
        .get("sourceIp", "unknown")
    )

    if not check_rate_limit(client_ip):
        return {
            "statusCode": 429,
            "body": json.dumps({
                "error": "Rate limit exceeded",
                "retry_after_seconds": 60
            })
        }

    # ... resto del handler

Ejemplo Completo: API Gateway → Lambda → OpenAI

Estructura del proyecto

lambda-ai-api/
├── src/
│   ├── handler.py       # POST /ask handler
│   ├── health.py        # GET /health handler
│   └── requirements.txt
├── template.yaml        # SAM template
└── samconfig.toml       # SAM deploy config

src/handler.py

import json
import os
import time
import logging
from openai import OpenAI

logger = logging.getLogger()
logger.setLevel(os.environ.get("LOG_LEVEL", "INFO"))

client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
    timeout=int(os.environ.get("LLM_TIMEOUT", "25")),
    max_retries=1,
)

CORS_HEADERS = {
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin": os.environ.get("ALLOWED_ORIGIN", "*"),
    "Access-Control-Allow-Methods": "POST, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type, X-Api-Key",
}

IS_COLD_START = True

def handler(event, context):
    global IS_COLD_START
    was_cold = IS_COLD_START
    IS_COLD_START = False

    start = time.time()

    method = event.get("requestContext", {}).get("http", {}).get("method", "")
    if method == "OPTIONS":
        return {"statusCode": 200, "headers": CORS_HEADERS, "body": ""}

    api_key = event.get("headers", {}).get("x-api-key", "")
    valid_keys = set(os.environ.get("API_KEYS", "").split(","))
    if valid_keys != {""} and api_key not in valid_keys:
        return {
            "statusCode": 401,
            "headers": CORS_HEADERS,
            "body": json.dumps({"error": "Unauthorized"})
        }

    try:
        body = json.loads(event.get("body", "{}"))
    except json.JSONDecodeError:
        return {
            "statusCode": 400,
            "headers": CORS_HEADERS,
            "body": json.dumps({"error": "Invalid JSON"})
        }

    prompt = body.get("prompt", "").strip()
    if not prompt:
        return {
            "statusCode": 400,
            "headers": CORS_HEADERS,
            "body": json.dumps({"error": "prompt is required"})
        }

    if len(prompt) > 10000:
        return {
            "statusCode": 400,
            "headers": CORS_HEADERS,
            "body": json.dumps({"error": "prompt too long (max 10000 chars)"})
        }

    max_tokens = min(body.get("max_tokens", 500), 2000)
    model = os.environ.get("MODEL_NAME", "gpt-4o-mini")

    remaining_ms = context.get_remaining_time_in_millis()
    if remaining_ms < 10000:
        model = "gpt-4o-mini"
        max_tokens = min(max_tokens, 200)

    try:
        llm_start = time.time()
        response = client.chat.completions.create(
            model=model,
            messages=[
                {"role": "system", "content": "Responde de forma concisa y útil."},
                {"role": "user", "content": prompt}
            ],
            max_tokens=max_tokens,
        )
        llm_ms = round((time.time() - llm_start) * 1000)

    except Exception as e:
        logger.error(f"LLM error: {e}")
        return {
            "statusCode": 502,
            "headers": CORS_HEADERS,
            "body": json.dumps({"error": f"LLM call failed: {str(e)}"})
        }

    total_ms = round((time.time() - start) * 1000)

    logger.info(json.dumps({
        "event": "ask",
        "cold_start": was_cold,
        "llm_ms": llm_ms,
        "total_ms": total_ms,
        "tokens": response.usage.total_tokens,
        "model": model,
    }))

    return {
        "statusCode": 200,
        "headers": CORS_HEADERS,
        "body": json.dumps({
            "answer": response.choices[0].message.content,
            "model": response.model,
            "tokens_used": response.usage.total_tokens,
            "duration_ms": total_ms,
            "cold_start": was_cold,
        })
    }

src/health.py

import json
import os
import time

def handler(event, context):
    start = time.time()

    checks = {"lambda": "up"}

    openai_key = os.environ.get("OPENAI_API_KEY", "")
    if openai_key and not openai_key.startswith("sk-"):
        checks["openai_key"] = "invalid_format"
    elif openai_key:
        checks["openai_key"] = "configured"
    else:
        checks["openai_key"] = "missing"

    overall = "healthy" if checks["openai_key"] == "configured" else "degraded"

    return {
        "statusCode": 200 if overall == "healthy" else 503,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({
            "status": overall,
            "checks": checks,
            "region": os.environ.get("AWS_REGION", "unknown"),
            "function": os.environ.get("AWS_LAMBDA_FUNCTION_NAME", "unknown"),
            "memory_mb": os.environ.get("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "unknown"),
            "latency_ms": round((time.time() - start) * 1000, 1)
        })
    }

src/requirements.txt

openai>=1.0.0

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: AI Endpoint with API Gateway

Globals:
  Function:
    Runtime: python3.11
    Architectures: [arm64]

Parameters:
  OpenAiApiKey:
    Type: String
    NoEcho: true
  AllowedOrigin:
    Type: String
    Default: "*"
  ApiKeys:
    Type: String
    Default: ""
    Description: Comma-separated API keys (empty = no auth)

Resources:
  AiApi:
    Type: AWS::Serverless::HttpApi
    Properties:
      StageName: prod
      CorsConfiguration:
        AllowOrigins:
          - !Ref AllowedOrigin
        AllowMethods: [POST, GET, OPTIONS]
        AllowHeaders: [Content-Type, X-Api-Key]
        MaxAge: 3600
      RouteSettings:
        "POST /ask":
          ThrottlingBurstLimit: 50
          ThrottlingRateLimit: 100

  AskFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: handler.handler
      CodeUri: ./src/
      MemorySize: 768
      Timeout: 60
      Environment:
        Variables:
          OPENAI_API_KEY: !Ref OpenAiApiKey
          MODEL_NAME: gpt-4o-mini
          LLM_TIMEOUT: "25"
          ALLOWED_ORIGIN: !Ref AllowedOrigin
          API_KEYS: !Ref ApiKeys
          LOG_LEVEL: INFO
      Events:
        Ask:
          Type: HttpApi
          Properties:
            ApiId: !Ref AiApi
            Path: /ask
            Method: POST

  HealthFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: health.handler
      CodeUri: ./src/
      MemorySize: 128
      Timeout: 5
      Environment:
        Variables:
          OPENAI_API_KEY: !Ref OpenAiApiKey
      Events:
        Health:
          Type: HttpApi
          Properties:
            ApiId: !Ref AiApi
            Path: /health
            Method: GET

Outputs:
  ApiUrl:
    Value: !Sub "https://${AiApi}.execute-api.${AWS::Region}.amazonaws.com/prod"
  AskEndpoint:
    Value: !Sub "https://${AiApi}.execute-api.${AWS::Region}.amazonaws.com/prod/ask"
  HealthEndpoint:
    Value: !Sub "https://${AiApi}.execute-api.${AWS::Region}.amazonaws.com/prod/health"

Probar el endpoint

# Deploy (si tienes cuenta AWS)
sam build && sam deploy --guided

# Test health
curl https://xyz.execute-api.us-east-1.amazonaws.com/prod/health

# Test ask
curl -X POST https://xyz.execute-api.us-east-1.amazonaws.com/prod/ask \
  -H "Content-Type: application/json" \
  -H "x-api-key: tu-key" \
  -d '{"prompt": "¿Qué es serverless?", "max_tokens": 200}'

# Test desde JavaScript (browser)
# fetch("https://xyz.execute-api.us-east-1.amazonaws.com/prod/ask", {
#   method: "POST",
#   headers: {"Content-Type": "application/json", "x-api-key": "tu-key"},
#   body: JSON.stringify({prompt: "Hola", max_tokens: 200})
# }).then(r => r.json()).then(console.log)

Troubleshooting

Problema 1: "CORS error en el browser"

# Síntoma: "Access to fetch has been blocked by CORS policy"
# El preflight OPTIONS no retorna los headers correctos

# Diagnóstico:
curl -v -X OPTIONS https://your-api.execute-api.us-east-1.amazonaws.com/prod/ask \
  -H "Origin: http://localhost:3000" \
  -H "Access-Control-Request-Method: POST"

# Si NO ves Access-Control-Allow-Origin en la respuesta:
# 1. Verifica CorsConfiguration en tu template.yaml
# 2. HTTP API maneja CORS automáticamente si está configurado
# 3. Si usas REST API, necesitas configurar OPTIONS method manualmente
# 4. Redeploy después de cambiar CORS config

Problema 2: "502 Bad Gateway" o "Internal Server Error"

# API Gateway no puede invocar tu Lambda o Lambda retornó un error

# Diagnóstico:
# 1. Revisa CloudWatch Logs de tu Lambda
aws logs tail /aws/lambda/AskFunction --follow

# 2. Verifica que el response format es correcto
# El body DEBE ser string, no dict
# ❌ {"statusCode": 200, "body": {"answer": "..."}}
# ✅ {"statusCode": 200, "body": "{\"answer\": \"...\"}"}

# 3. Verifica permisos: API Gateway necesita permiso para invocar Lambda
# SAM lo configura automáticamente, pero con CLI manual necesitas:
aws lambda add-permission \
  --function-name ai-endpoint \
  --statement-id apigateway \
  --action lambda:InvokeFunction \
  --principal apigateway.amazonaws.com

Problema 3: "Timeout 504 — Endpoint request timed out"

# API Gateway timeout es 29 segundos (hard limit)
# Si tu Lambda tarda más, Gateway retorna 504

# Soluciones:
# 1. Optimiza tu Lambda (modelo más rápido, menos tokens)
# 2. Usa patrón async (retorna requestId, procesa en background)
# 3. Invoca Lambda directamente (sin Gateway) para workloads largos

# Si la latencia está entre 25-29s, es un problema de timing.
# Configura el client timeout de OpenAI a 20s para detectar
# el timeout del LLM antes de que Gateway te corte.

Problema 4: "Missing Authentication Token" (REST API)

# Si usas REST API (v1) y ves este error,
# probablemente estás invocando un path que no existe.
# REST API retorna 403 "Missing Authentication Token" para rutas inexistentes
# (confuso, pero así funciona).

# Verifica:
aws apigateway get-resources --rest-api-id API_ID
# Asegúrate de que tu ruta existe y el método está configurado

Problema 5: "La respuesta llega truncada"

# API Gateway tiene un límite de payload de 10MB
# Pero el issue más común es que tu Lambda retorna un body
# que no es string serializado

# Verifica que json.dumps() envuelve todo el body:
# ✅ "body": json.dumps({"answer": answer, "tokens": 42})
# ❌ "body": {"answer": answer, "tokens": 42}

Ejercicios Prácticos

Ejercicio 1: Agrega un endpoint GET /models

Crea un endpoint GET /models que retorne los modelos disponibles, su coste estimado por 1K tokens, y el max_tokens permitido. No requiere autenticación.

Ver solución
# src/models.py
import json

AVAILABLE_MODELS = {
    "gpt-4o-mini": {
        "cost_per_1k_input": 0.00015,
        "cost_per_1k_output": 0.0006,
        "max_tokens": 4096,
        "description": "Fast and cheap, good for most tasks"
    },
    "gpt-4o": {
        "cost_per_1k_input": 0.0025,
        "cost_per_1k_output": 0.01,
        "max_tokens": 4096,
        "description": "Most capable, higher cost"
    },
}

def handler(event, context):
    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({
            "models": AVAILABLE_MODELS,
            "default": "gpt-4o-mini"
        })
    }

En el template.yaml, agrega:

  ModelsFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: models.handler
      CodeUri: ./src/
      MemorySize: 128
      Timeout: 5
      Events:
        Models:
          Type: HttpApi
          Properties:
            ApiId: !Ref AiApi
            Path: /models
            Method: GET

Ejercicio 2: Implementa request validation

Modifica el handler de /ask para validar: prompt no vacío, max_tokens entre 1-2000, y que el model solicitado exista en la lista de modelos disponibles. Retorna errores descriptivos con status 400.

Ver solución
import json
import os
from openai import OpenAI

VALID_MODELS = {"gpt-4o-mini", "gpt-4o"}
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"], timeout=25)

def validate_request(body):
    errors = []

    prompt = body.get("prompt", "").strip()
    if not prompt:
        errors.append("prompt is required and cannot be empty")
    elif len(prompt) > 10000:
        errors.append(f"prompt too long: {len(prompt)} chars (max 10000)")

    max_tokens = body.get("max_tokens", 500)
    if not isinstance(max_tokens, int) or max_tokens < 1 or max_tokens > 2000:
        errors.append("max_tokens must be integer between 1 and 2000")

    model = body.get("model", "gpt-4o-mini")
    if model not in VALID_MODELS:
        errors.append(f"model '{model}' not available. Valid: {', '.join(VALID_MODELS)}")

    return errors

def handler(event, context):
    try:
        body = json.loads(event.get("body", "{}"))
    except json.JSONDecodeError:
        return {
            "statusCode": 400,
            "body": json.dumps({"errors": ["Invalid JSON body"]})
        }

    errors = validate_request(body)
    if errors:
        return {
            "statusCode": 400,
            "body": json.dumps({"errors": errors})
        }

    response = client.chat.completions.create(
        model=body.get("model", "gpt-4o-mini"),
        messages=[{"role": "user", "content": body["prompt"]}],
        max_tokens=body.get("max_tokens", 500),
    )

    return {
        "statusCode": 200,
        "body": json.dumps({
            "answer": response.choices[0].message.content,
            "tokens_used": response.usage.total_tokens,
        })
    }

Ejercicio 3: Patrón async para workloads largos

Implementa dos endpoints: POST /ask-async que inicia el procesamiento y retorna un request_id, y GET /status/{request_id} que retorna el estado. Usa un dict en memoria como store simplificado (en producción usarías DynamoDB).

Ver solución
# src/ask_async.py
import json
import os
import uuid
import threading
import time
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"], timeout=50)

results_store = {}

def process_llm(request_id, prompt, max_tokens):
    try:
        results_store[request_id]["status"] = "processing"
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
            max_tokens=max_tokens,
        )
        results_store[request_id] = {
            "status": "completed",
            "answer": response.choices[0].message.content,
            "tokens_used": response.usage.total_tokens,
            "completed_at": time.time(),
        }
    except Exception as e:
        results_store[request_id] = {
            "status": "failed",
            "error": str(e),
        }

def handler(event, context):
    route = event.get("routeKey", "")

    if route.startswith("POST"):
        body = json.loads(event.get("body", "{}"))
        request_id = str(uuid.uuid4())[:8]

        results_store[request_id] = {"status": "queued"}

        thread = threading.Thread(
            target=process_llm,
            args=(request_id, body["prompt"], body.get("max_tokens", 500))
        )
        thread.start()

        return {
            "statusCode": 202,
            "body": json.dumps({
                "request_id": request_id,
                "status": "queued",
                "check_url": f"/status/{request_id}"
            })
        }

    elif route.startswith("GET"):
        request_id = event.get("pathParameters", {}).get("request_id", "")
        result = results_store.get(request_id)

        if not result:
            return {
                "statusCode": 404,
                "body": json.dumps({"error": "Request not found"})
            }

        status_code = 200 if result["status"] == "completed" else 202
        return {
            "statusCode": status_code,
            "body": json.dumps(result)
        }

En template.yaml:

  AsyncFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: ask_async.handler
      CodeUri: ./src/
      MemorySize: 768
      Timeout: 120
      Events:
        Submit:
          Type: HttpApi
          Properties:
            Path: /ask-async
            Method: POST
        Status:
          Type: HttpApi
          Properties:
            Path: /status/{request_id}
            Method: GET

Ejercicio 4: Custom domain con API Gateway

Documenta (sin implementar) los pasos para conectar un custom domain (api.tuapp.com) a tu HTTP API. Incluye: certificado ACM, configuración de domain name en API Gateway, y DNS record en Route 53 o tu DNS provider.

Ver solución
# Paso 1: Crear certificado SSL en ACM (us-east-1 para API Gateway)
# AWS Console → Certificate Manager → Request certificate
# Domain: api.tuapp.com
# Validación: DNS (agrega el CNAME que ACM te da)

# Paso 2: Configurar custom domain en API Gateway
Resources:
  CustomDomain:
    Type: AWS::ApiGatewayV2::DomainName
    Properties:
      DomainName: api.tuapp.com
      DomainNameConfigurations:
        - CertificateArn: arn:aws:acm:us-east-1:ACCOUNT:certificate/CERT-ID
          EndpointType: REGIONAL

  ApiMapping:
    Type: AWS::ApiGatewayV2::ApiMapping
    Properties:
      ApiId: !Ref AiApi
      DomainName: !Ref CustomDomain
      Stage: prod

# Paso 3: DNS Record
# En Route 53 o tu DNS provider:
# Type: CNAME (o ALIAS en Route 53)
# Name: api.tuapp.com
# Value: d-XXXXXXXX.execute-api.us-east-1.amazonaws.com
#         ↑ Este valor lo obtienes del custom domain en API Gateway

# Paso 4: Verificar
# curl https://api.tuapp.com/health
# Debe retornar el health check de tu Lambda
# Con AWS CLI:
# 1. Crear domain name
aws apigatewayv2 create-domain-name \
  --domain-name api.tuapp.com \
  --domain-name-configurations CertificateArn=arn:aws:acm:us-east-1:ACCOUNT:certificate/ID

# 2. Crear mapping
aws apigatewayv2 create-api-mapping \
  --api-id API_ID \
  --domain-name api.tuapp.com \
  --stage prod

# 3. Obtener target domain para DNS
aws apigatewayv2 get-domain-name --domain-name api.tuapp.com
# → ApiGatewayDomainName: d-XXXXXXXX.execute-api.us-east-1.amazonaws.com

# 4. Configurar CNAME en tu DNS

Resumen

  • API Gateway convierte tu Lambda en un endpoint HTTP. Sin Gateway, tu Lambda no es accesible desde internet.
  • HTTP API (v2) es la opción correcta para AI endpoints — 3.5x más barato, menor latencia, configuración más simple.
  • El timeout de API Gateway es 29 segundos (hard limit). Para workloads más largos, usa invocación directa o patrón async.
  • CORS debe configurarse en API Gateway (no solo en tu Lambda) para que browsers puedan llamar tu endpoint.
  • El body del response Lambda DEBE ser string (json.dumps()), no dict — es el error más común.
  • Autenticación: API key para desarrollo, JWT para producción con usuarios, IAM para service-to-service.
  • Rate limiting: configura throttling en API Gateway Y protege tu cuenta de OpenAI con rate limits en tu código.
  • Payload format 2.0 (HTTP API): el evento tiene estructura diferente a REST API. Usa event["requestContext"]["http"]["method"] para el método.

Recursos Adicionales

  1. HTTP API (v2) Documentation — Referencia completa de HTTP API
  2. REST API vs HTTP API — Comparación oficial
  3. CORS Configuration — Configurar CORS en HTTP API
  4. Lambda Proxy Integration — Integración Lambda con payload v2.0
  5. JWT Authorizers — Autenticación JWT en HTTP API
  6. API Gateway Throttling — Rate limiting y throttling
  7. SAM Template Reference — Referencia SAM
  8. Custom Domain Names — Configurar dominio custom