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 fromhttps://app.example.comtalk tohttps://api.example.comunless the API explicitly putsapp.example.comon the guest list via response headers. The bouncer checks the list before letting the conversation happen — that pre-check is the HTTPOPTIONSpreflight request.
The CORS Request Flow: What Actually Happens
Understanding the two-phase browser handshake is critical before touching any configuration.
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 ✅
- Preflight (OPTIONS): For non-simple requests (e.g., POST with JSON, custom headers), the browser automatically sends an
OPTIONSrequest to the same URL before the real request. This is not your code — the browser does it automatically. - API Gateway OPTIONS Handler: API Gateway must have an
OPTIONSmethod configured on the resource. The "Enable CORS" wizard in the console creates this for you with a mock integration. - 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. - Actual Request: Only after a successful preflight does the browser send the real
GET/POST/etc. request to API Gateway, which invokes your Lambda. - 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
- Open the API Gateway console and select your REST API.
- In the left panel, click Resources and select the resource path (e.g.,
/users). - From the Actions dropdown, select Enable CORS.
- 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).
- Access-Control-Allow-Origin: Set to your frontend origin (e.g.,
- Click Enable CORS and replace existing CORS headers. This creates the
OPTIONSmethod with a mock integration and injectsAccess-Control-Allow-*headers into the method responses of your existing methods. - CRITICAL: Click Actions → Deploy API and deploy to your stage. Without this step, no changes are live.
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
- The Enable CORS wizard operates at the resource level — you must repeat this for each resource path that your frontend calls.
- The wizard creates a Mock Integration for the
OPTIONSmethod, meaning API Gateway responds to preflight requests directly without invoking Lambda — this is correct and intentional. - 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
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
- Missing OPTIONS method: The "Enable CORS" step was skipped or not deployed. The browser's preflight gets a 403 or 404, blocking everything downstream.
- 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. - Origin mismatch: Lambda returns
Access-Control-Allow-Origin: https://app.example.combut the browser is running onhttp://localhost:3000. These are different origins. Use environment variables to manage this per stage. - Credentials + wildcard conflict: If your frontend sends
credentials: 'include', the server cannot respond withAccess-Control-Allow-Origin: *. You must specify the exact origin and also returnAccess-Control-Allow-Credentials: true. - 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: UsingAccess-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 likeAdministratorAccess. - 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.
- 📖 Official Docs: Enabling CORS for a REST API resource — AWS Documentation
- 📖 HTTP API CORS: Configuring CORS for an HTTP API — AWS Documentation
- 🔍 Next: If you are managing this via Infrastructure as Code, explore the
AWS::ApiGateway::MethodCloudFormation resource or the AWS SAMCorsproperty onAWS::Serverless::Apito codify this configuration and eliminate manual console steps.
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
Post a Comment