API Gateway CORS Errors: How to Enable CORS and Fix Lambda Response Headers

A CORS error on an API Gateway endpoint is one of the most common — and most misdiagnosed — issues in serverless frontend development. The browser blocks the request before your Lambda even runs, yet engineers instinctively look at Lambda logs first. Understanding where CORS enforcement actually lives is the only way to fix it correctly.

TL;DR: API Gateway CORS Fix at a Glance

LayerWhat You ConfigureWhy It Matters
API Gateway (REST)Enable CORS per resource — adds OPTIONS mock integrationHandles browser preflight (OPTIONS) request
API Gateway (HTTP)CORS configuration on the API levelAutomatically injects CORS headers on all responses
Lambda responseReturn CORS headers in every response bodyRequired for actual GET/POST/PUT responses after preflight

Why API Gateway CORS Errors Happen

Before any cross-origin POST or PUT, the browser sends a preflight OPTIONS request to ask the server: "Do you allow requests from this origin?" If API Gateway doesn't respond to that OPTIONS request with the correct headers, the browser never sends the real request. Your Lambda never fires. Your CloudWatch logs stay empty. The error lives entirely in the API Gateway layer.

Think of the preflight as a bouncer check at the door. Your Lambda is the event inside. If the bouncer (API Gateway) turns the browser away, the event never starts — no matter how well your Lambda is written.

There are two distinct API Gateway types with different CORS configuration paths. Mixing up the steps between them is the most common source of persistent CORS failures after "fixing" it.

sequenceDiagram participant Browser participant APIGW as API Gateway participant Lambda Browser->>APIGW: OPTIONS /items (Preflight) alt CORS not configured APIGW-->>Browser: 403 / No CORS headers Note over Browser: Browser blocks request else CORS configured APIGW-->>Browser: 200 + Access-Control-Allow-* headers Browser->>APIGW: POST /items (Actual Request) APIGW->>Lambda: Invoke function Lambda-->>APIGW: 200 response APIGW-->>Browser: Response (CORS headers required here too) end
  1. Browser sends preflight OPTIONS — triggered automatically for cross-origin requests with custom headers or non-simple methods.
  2. API Gateway must respond to OPTIONS — without invoking Lambda. This is the mock integration step.
  3. Browser sends actual request — only if preflight succeeds.
  4. Lambda must include CORS headers in its response — API Gateway passes the response through; headers missing here cause a second class of CORS failures.

Solution A: Enable CORS on REST API (Console + CLI)

REST API is the older, more explicit type. CORS is not automatic — you configure it per resource.

Step 1 — Enable CORS via Console

  1. Open the API Gateway console → select your REST API.
  2. In the left panel, select the resource (e.g., /items).
  3. Click Actions → Enable CORS.
  4. Set Access-Control-Allow-Origin to your frontend origin (e.g., https://app.example.com) or * for open access.
  5. Confirm the headers and methods listed, then click Enable CORS and replace existing CORS headers.
  6. Deploy the API to a stage — changes are not live until deployed.

What this does: API Gateway creates an OPTIONS method on the resource with a mock integration that returns the configured CORS headers. It also adds Access-Control-Allow-Origin to the method response of your existing methods (GET, POST, etc.).

Step 2 — Enable CORS via CLI (REST API)

The console wizard is convenient but opaque. For repeatable infrastructure, use the CLI. The sequence is: create OPTIONS method → put mock integration → put method response → put integration response.

🔽 Click to expand — CLI: Add OPTIONS mock integration to REST API resource
# 1. Create OPTIONS method on the resource
aws apigateway put-method \
  --rest-api-id a1b2c3d4e5 \
  --resource-id abc123 \
  --http-method OPTIONS \
  --authorization-type NONE \
  --region us-east-1

# 2. Put a MOCK integration (no Lambda invocation)
aws apigateway put-integration \
  --rest-api-id a1b2c3d4e5 \
  --resource-id abc123 \
  --http-method OPTIONS \
  --type MOCK \
  --request-templates '{"application/json": "{\"statusCode\": 200}"}' \
  --region us-east-1

# 3. Put method response for 200
aws apigateway put-method-response \
  --rest-api-id a1b2c3d4e5 \
  --resource-id abc123 \
  --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 us-east-1

# 4. Put integration response with actual header values
aws apigateway put-integration-response \
  --rest-api-id a1b2c3d4e5 \
  --resource-id abc123 \
  --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.example.com'\''"}' \
  --region us-east-1

# 5. Deploy to stage
aws apigateway create-deployment \
  --rest-api-id a1b2c3d4e5 \
  --stage-name prod \
  --region us-east-1

Solution B: Enable CORS on HTTP API (Console + CLI)

HTTP API (the newer, lower-latency type) has a dedicated CORS configuration block at the API level. When configured, API Gateway automatically handles OPTIONS preflight responses and injects CORS headers — you do not need to create OPTIONS routes manually.

Console Steps

  1. Open API Gateway console → select your HTTP API.
  2. Go to Develop → CORS.
  3. Set Access-Control-Allow-Origin, Allow Headers, Allow Methods, and optionally Expose Headers and Max Age.
  4. Save. No manual deployment step is required for CORS configuration changes on HTTP APIs.

CLI: Configure CORS on HTTP API

aws apigatewayv2 update-api \
  --api-id a1b2c3d4e5 \
  --cors-configuration AllowOrigins='https://app.example.com',AllowMethods='GET,POST,OPTIONS',AllowHeaders='Content-Type,Authorization',MaxAge=300 \
  --region us-east-1

Verify the configuration was applied:

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

Step 3 — Lambda Must Return CORS Headers in Every Response

This is where engineers get burned after thinking they've fixed it. The OPTIONS preflight succeeds. The browser sends the real GET or POST. Lambda returns a 200 — but without CORS headers in the response. The browser blocks it anyway.

For REST API, API Gateway does not automatically inject CORS headers into Lambda responses. Your Lambda function must explicitly include them.

flowchart TD A["Browser sends POST"] --> B["API Gateway receives request"] B --> C["Lambda invoked"] C --> D{"Lambda response
includes CORS headers?"} D -- No --> E["API Gateway forwards response
without CORS headers"] E --> F["Browser blocks response
CORS error in console"] D -- Yes --> G["API Gateway forwards response
with CORS headers"] G --> H["Browser accepts response"]
  1. Preflight succeeds — API Gateway mock integration returns CORS headers correctly.
  2. Actual request sent — Lambda is invoked.
  3. Lambda response missing headers — Browser receives 200 but blocks the response due to missing Access-Control-Allow-Origin.
  4. Lambda response with headers — Browser accepts the response.

Lambda Response Format (Node.js)

exports.handler = async (event) => {
  return {
    statusCode: 200,
    headers: {
      'Access-Control-Allow-Origin': 'https://app.example.com',
      'Access-Control-Allow-Headers': 'Content-Type,Authorization',
      'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ message: 'Success' })
  };
};

Lambda Response Format (Python)

import json

def lambda_handler(event, context):
    return {
        'statusCode': 200,
        'headers': {
            'Access-Control-Allow-Origin': 'https://app.example.com',
            'Access-Control-Allow-Headers': 'Content-Type,Authorization',
            'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
            'Content-Type': 'application/json'
        },
        'body': json.dumps({'message': 'Success'})
    }

For HTTP API, when the CORS configuration is set at the API level, API Gateway merges CORS headers into responses automatically. However, if your Lambda returns a header that conflicts with the API-level configuration, the behavior depends on which header takes precedence — verify your specific setup against the AWS documentation.

Diagnosing API Gateway CORS Errors: Step-by-Step

The symptom is always the same: the browser console shows a CORS error. The cause is almost never where you first look.

Step 1 — Confirm the API type

Operator Rationale: REST API and HTTP API have entirely different CORS configuration paths. Applying REST API steps to an HTTP API (or vice versa) produces no error but also no fix.

# List all APIs and their ProtocolType
aws apigateway get-rest-apis --region us-east-1
aws apigatewayv2 get-apis --region us-east-1

Step 2 — Test the OPTIONS preflight directly

Operator Rationale: If OPTIONS returns a non-200 or missing headers, the browser never sends the real request. This isolates whether the problem is in the preflight layer or the Lambda response layer.

curl -v -X OPTIONS https://a1b2c3d4e5.execute-api.us-east-1.amazonaws.com/prod/items \
  -H 'Origin: https://app.example.com' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: Content-Type,Authorization'

Expected: HTTP 200 with Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers in the response headers.

Step 3 — Test the actual method response headers

Operator Rationale: A passing preflight with a failing actual request means Lambda is not returning CORS headers. This is the most common second-stage failure.

curl -v -X POST https://a1b2c3d4e5.execute-api.us-east-1.amazonaws.com/prod/items \
  -H 'Origin: https://app.example.com' \
  -H 'Content-Type: application/json' \
  -d '{"key": "value"}'

Check the response headers for Access-Control-Allow-Origin. If absent, the fix is in your Lambda response code, not API Gateway configuration.

Step 4 — Verify REST API is deployed

Operator Rationale: REST API changes are not live until explicitly deployed to a stage. This is the single most common reason a CORS fix appears correct in the console but has no effect in production.

aws apigateway get-stages \
  --rest-api-id a1b2c3d4e5 \
  --region us-east-1

Compare the lastUpdatedDate of your stage against when you made the CORS change. If the stage predates your change, redeploy.

Step 5 — Check for Lambda Proxy integration on REST API

Operator Rationale: With Lambda Proxy integration, API Gateway passes the Lambda response directly to the client without modifying headers. The "Enable CORS" console wizard adds headers to the method response configuration — but with proxy integration, those method response headers are bypassed. Your Lambda must return the headers itself.

aws apigateway get-integration \
  --rest-api-id a1b2c3d4e5 \
  --resource-id abc123 \
  --http-method POST \
  --region us-east-1 \
  --query 'type'

If the output is AWS_PROXY, your Lambda response must include all CORS headers explicitly. The method response header configuration in API Gateway has no effect on proxy integrations.

Experience Signal: The Silent Proxy Integration Override

A team enables CORS via the console wizard on a REST API. The OPTIONS preflight works. The POST still fails with a CORS error. CloudWatch shows Lambda returning 200. The console shows CORS headers configured on the method response. Everything looks correct.

The actual cause: the POST method uses AWS_PROXY integration. With proxy integration, API Gateway does not apply the method response header mappings — it forwards the Lambda response as-is. The CORS headers configured in the console are real, but they only apply to non-proxy integrations. Lambda was returning a plain { statusCode: 200, body: '...' } with no headers object.

The fix: add the headers object to the Lambda return value. The console configuration was irrelevant for this integration type.

Insight: the console wizard succeeds silently even when it configures headers that the integration type will never use.

Required CORS Headers Reference

HeaderRequired ForExample Value
Access-Control-Allow-OriginAll CORS responseshttps://app.example.com or *
Access-Control-Allow-MethodsPreflight (OPTIONS) responseGET,POST,PUT,OPTIONS
Access-Control-Allow-HeadersPreflight — when request uses custom headersContent-Type,Authorization
Access-Control-Max-AgeOptional — preflight cache duration (seconds)300
Access-Control-Expose-HeadersOptional — headers browser JS can readX-Request-Id
Access-Control-Allow-CredentialsRequired when sending cookies or auth headerstrue (cannot combine with * origin)

When Access-Control-Allow-Credentials: true is required, Access-Control-Allow-Origin must be an explicit origin — not *. Browsers enforce this strictly.

IAM Permissions for API Gateway Management

🔽 Click to expand — IAM policy for managing API Gateway CORS configuration
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RestApiCORSManagement",
      "Effect": "Allow",
      "Action": [
        "apigateway:GET",
        "apigateway:PUT",
        "apigateway:POST",
        "apigateway:PATCH",
        "apigateway:DELETE"
      ],
      "Resource": [
        "arn:aws:apigateway:us-east-1::/restapis/a1b2c3d4e5/*"
      ]
    },
    {
      "Sid": "HttpApiCORSManagement",
      "Effect": "Allow",
      "Action": [
        "apigateway:GET",
        "apigateway:PATCH"
      ],
      "Resource": [
        "arn:aws:apigateway:us-east-1::/apis/a1b2c3d4e5"
      ]
    }
  ]
}

Wrap-Up: Fixing API Gateway CORS Errors the Right Way

API Gateway CORS errors require fixes at two independent layers: the preflight OPTIONS handling in API Gateway, and the CORS headers in your Lambda response. Fixing only one layer produces a partial fix that fails in ways that look identical to a complete failure in the browser console.

Key actions:

  • Identify your API type (REST vs. HTTP) before applying any fix.
  • For REST API with Lambda Proxy integration, Lambda must return CORS headers — console wizard configuration alone is insufficient.
  • Always redeploy a REST API stage after CORS changes.
  • Use curl -v -X OPTIONS to isolate preflight failures from actual response failures.
  • Never use Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true.

For authoritative configuration details, refer to the AWS API Gateway CORS documentation.

Glossary

TermDefinition
Preflight RequestAn HTTP OPTIONS request sent by the browser before a cross-origin request to verify the server permits it.
CORS (Cross-Origin Resource Sharing)A browser security mechanism that restricts cross-origin HTTP requests unless the server explicitly permits them via response headers.
Lambda Proxy IntegrationAn API Gateway integration type (AWS_PROXY) where the Lambda function controls the full HTTP response, including status code and headers.
Mock IntegrationAn API Gateway integration that returns a configured response without invoking any backend — used for OPTIONS preflight handling in REST APIs.
HTTP API vs. REST APITwo distinct API Gateway types. HTTP API has native CORS configuration; REST API requires manual OPTIONS method and mock integration setup.

Related Posts

Comments

Popular posts from this blog

EC2 No Internet Access in Custom VPC: Fix Internet Gateway and Route Table

EC2 SSH Connection Timeout: Which Security Group Rules to Check

Difference Between IAM User and IAM Role: Which One Should Your EC2 Use?