Fixing API Gateway CORS Errors: Enable CORS in the Console & Required Lambda Response Headers

Your React or Vue frontend fires a fetch request to your shiny new API Gateway endpoint — and the browser immediately kills it with a CORS error before your Lambda even gets a chance to respond. This is one of the most common and frustrating deployment blockers for serverless APIs, and it stems from a two-part configuration requirement that most tutorials only half-explain.

TL;DR

Step Where What You Must Do
1. Enable CORS on the Resource API Gateway Console Add an OPTIONS method with a mock integration that returns the correct preflight headers
2. Deploy the API API Gateway Console Create or update a Stage deployment — CORS config is NOT live until you deploy
3. Return CORS Headers from Lambda Your Lambda function code Include Access-Control-Allow-Origin and related headers in the headers object of every Lambda response

Why CORS Exists: The Browser's Security Guard

Analogy: CORS is like a nightclub bouncer enforcing a guest list. Your browser (the bouncer) will not let a script from https://app.example.com talk to https://api.example.com unless the API explicitly puts app.example.com on the guest list via response headers. The bouncer checks the list before letting the conversation happen — that pre-check is the HTTP OPTIONS preflight request.

The CORS Request Flow: What Actually Happens

Understanding the two-phase browser handshake is critical before touching any configuration.

sequenceDiagram participant Browser as "Browser" participant APIGW as "API Gateway" participant Lambda as "Lambda Function" Browser->>APIGW: 1. OPTIONS /users (Preflight) Note over Browser,APIGW: Headers: Origin, Access-Control-Request-Method APIGW-->>Browser: 2. 200 OK (Mock Integration) Note over APIGW,Browser: Headers: Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers Browser->>APIGW: 3. POST /users (Actual Request) APIGW->>Lambda: 4. Invoke Lambda Lambda-->>APIGW: 5. Response with CORS headers APIGW-->>Browser: 6. 200 OK Note over APIGW,Browser: Headers: Access-Control-Allow-Origin (REQUIRED) Browser->>Browser: 7. JavaScript reads response ✅
  1. Preflight (OPTIONS): For non-simple requests (e.g., POST with JSON, custom headers), the browser automatically sends an OPTIONS request to the same URL before the real request. This is not your code — the browser does it automatically.
  2. API Gateway OPTIONS Handler: API Gateway must have an OPTIONS method configured on the resource. The "Enable CORS" wizard in the console creates this for you with a mock integration.
  3. Preflight Response: API Gateway returns the preflight response with headers like Access-Control-Allow-Origin. If these are missing or wrong, the browser blocks the actual request entirely.
  4. Actual Request: Only after a successful preflight does the browser send the real GET/POST/etc. request to API Gateway, which invokes your Lambda.
  5. Lambda Response with CORS Headers: Your Lambda must also return CORS headers in its response. The preflight only unlocks the door — your Lambda's response must confirm the same policy or the browser will still block the response.

Part 1: Enable CORS in the API Gateway Console (REST API)

These steps apply to REST APIs in API Gateway. HTTP APIs have a different, simpler CORS configuration panel.

Step-by-Step: REST API

  1. Open the API Gateway console and select your REST API.
  2. In the left panel, click Resources and select the resource path (e.g., /users).
  3. From the Actions dropdown, select Enable CORS.
  4. Configure the fields:
    • Access-Control-Allow-Origin: Set to your frontend origin (e.g., https://app.example.com). Use * only for fully public, unauthenticated APIs.
    • Access-Control-Allow-Headers: Include any custom headers your frontend sends, e.g., Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token.
    • Access-Control-Allow-Methods: Select the HTTP methods your resource supports (e.g., GET,POST,OPTIONS).
  5. Click Enable CORS and replace existing CORS headers. This creates the OPTIONS method with a mock integration and injects Access-Control-Allow-* headers into the method responses of your existing methods.
  6. CRITICAL: Click Actions → Deploy API and deploy to your stage. Without this step, no changes are live.
graph TD A["Select Resource
e.g. /users"] --> B["Actions → Enable CORS"] B --> C["Configure Allow-Origin
Allow-Headers, Allow-Methods"] C --> D["Click: Enable CORS and
replace existing CORS headers"] D --> E["API Gateway creates
OPTIONS Mock Integration"] E --> F["Actions → Deploy API"] F --> G["Select or Create Stage
e.g. prod"] G --> H["✅ CORS Config is LIVE"] style H fill:#2ecc71,color:#fff style F fill:#e74c3c,color:#fff
  1. The Enable CORS wizard operates at the resource level — you must repeat this for each resource path that your frontend calls.
  2. The wizard creates a Mock Integration for the OPTIONS method, meaning API Gateway responds to preflight requests directly without invoking Lambda — this is correct and intentional.
  3. The Deploy API step is a hard requirement. API Gateway stages are immutable snapshots; edits to resources are staged changes until explicitly deployed.

HTTP API (Simpler Alternative)

If you are using an HTTP API (not REST API), CORS is configured at the API level, not per-resource. Navigate to your HTTP API → CORS in the left panel, and configure Allow Origins, Allow Methods, and Allow Headers directly. API Gateway handles the OPTIONS preflight automatically.

Part 2: Required CORS Headers in Your Lambda Response

This is the step most developers miss. Even after configuring CORS on API Gateway, your Lambda function must return CORS headers in its own response for every non-OPTIONS request. API Gateway's CORS configuration handles the preflight; your Lambda handles the actual response.

The Minimum Required Headers

Header Required? Example Value
Access-Control-Allow-Origin ✅ Always https://app.example.com
Access-Control-Allow-Headers ⚠️ If using custom headers Content-Type,Authorization
Access-Control-Allow-Methods ⚠️ Recommended GET,POST,OPTIONS
Access-Control-Allow-Credentials ⚠️ Only if sending cookies/auth true (cannot combine with * origin)

Lambda Response Code Examples

🔽 Python (boto3 / Lambda handler)
import json

CORS_HEADERS = {
    "Access-Control-Allow-Origin": "https://app.example.com",
    "Access-Control-Allow-Headers": "Content-Type,Authorization,X-Amz-Date,X-Api-Key",
    "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS"
}

def lambda_handler(event, context):
    # Your business logic here
    body = {"message": "Hello from Lambda"}

    return {
        "statusCode": 200,
        "headers": CORS_HEADERS,
        "body": json.dumps(body)
    }
🔽 Node.js (Lambda handler)
const CORS_HEADERS = {
  "Access-Control-Allow-Origin": "https://app.example.com",
  "Access-Control-Allow-Headers": "Content-Type,Authorization,X-Amz-Date,X-Api-Key",
  "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS"
};

exports.handler = async (event) => {
  // Your business logic here
  const body = { message: "Hello from Lambda" };

  return {
    statusCode: 200,
    headers: CORS_HEADERS,
    body: JSON.stringify(body)
  };
};

Error Response — Don't Forget CORS Headers Here Too

A critical and commonly overlooked detail: if your Lambda throws an error and returns a non-2xx status code, that response also needs CORS headers. Without them, the browser will suppress the actual error message, making debugging nearly impossible.

# Python error response example
return {
    "statusCode": 400,
    "headers": CORS_HEADERS,  # Include CORS headers even on errors
    "body": json.dumps({"error": "Invalid request payload"})
}

Common Failure Scenarios & Diagnostics

graph TD Start(["Browser Makes
Cross-Origin Request"]) --> Pre{"Is it a
Preflight?"} Pre -->|"Yes - OPTIONS"| OPT{"OPTIONS method
exists on APIGW?"} OPT -->|"No"| ERR1["❌ 403/404 Error
CORS Blocked"] OPT -->|"Yes"| HDR{"Correct Allow-Origin
header returned?"} HDR -->|"No / Mismatch"| ERR2["❌ Preflight Failed
Origin not allowed"] HDR -->|"Yes"| ACT["Browser sends
Actual Request"] Pre -->|"No - Simple Request"| ACT ACT --> LAM["Lambda Invoked"] LAM --> LHDR{"Lambda response
includes CORS headers?"} LHDR -->|"No"| ERR3["❌ Response Blocked
by Browser"] LHDR -->|"Yes - Origin matches"| OK["✅ JavaScript receives
response successfully"] style ERR1 fill:#e74c3c,color:#fff style ERR2 fill:#e74c3c,color:#fff style ERR3 fill:#e74c3c,color:#fff style OK fill:#2ecc71,color:#fff
  1. Missing OPTIONS method: The "Enable CORS" step was skipped or not deployed. The browser's preflight gets a 403 or 404, blocking everything downstream.
  2. Lambda missing CORS headers: Preflight succeeds (API Gateway handles it), but the actual response from Lambda has no Access-Control-Allow-Origin. The browser receives the response but refuses to expose it to your JavaScript.
  3. Origin mismatch: Lambda returns Access-Control-Allow-Origin: https://app.example.com but the browser is running on http://localhost:3000. These are different origins. Use environment variables to manage this per stage.
  4. Credentials + wildcard conflict: If your frontend sends credentials: 'include', the server cannot respond with Access-Control-Allow-Origin: *. You must specify the exact origin and also return Access-Control-Allow-Credentials: true.
  5. Not redeployed: Changes to API Gateway resources are not live until the API is redeployed to a stage.

IAM & Security Considerations

  • Avoid * for authenticated APIs: Using Access-Control-Allow-Origin: * on an API that uses Cognito or API keys is a security misconfiguration. Always specify the exact allowed origin(s).
  • Lambda execution role: The Lambda function's IAM execution role should follow least privilege. For a basic API function, the minimum is AWSLambdaBasicExecutionRole (CloudWatch Logs write access). Do not attach broad policies like AdministratorAccess.
  • API Gateway resource policy: For additional origin-level access control at the infrastructure layer, consider an API Gateway resource policy — though this is separate from CORS and operates at the IAM authorization level.

Wrap-Up & Next Steps

CORS errors in API Gateway are always a two-part fix: configure the OPTIONS preflight response in API Gateway, and return the correct headers from your Lambda function on every response — including errors. Missing either half will result in a browser block.

Glossary

Term Definition
CORS Cross-Origin Resource Sharing — a browser security mechanism that restricts HTTP requests made from one origin to a different origin unless the server explicitly permits it.
Preflight Request An automatic HTTP OPTIONS request sent by the browser before a cross-origin request to verify the server allows the actual request method and headers.
Mock Integration An API Gateway integration type where the gateway itself generates the response without invoking a backend. Used for the OPTIONS method to handle preflight without Lambda invocation cost.
Stage Deployment A named snapshot of an API Gateway configuration (e.g., dev, prod). Resource changes are not live until explicitly deployed to a stage.
Origin The combination of protocol, hostname, and port (e.g., https://app.example.com:443). Two URLs are cross-origin if any of these three components differ.

Comments

Popular posts from this blog

EC2 No Internet Access in Custom VPC: Attaching an Internet Gateway and Fixing Route Tables

IAM User vs. IAM Role: Why Your EC2 Instance Should Never Use a User

Lambda Infinite Loop with S3: How to Prevent Recursive Triggers