GuidesPLATFORMS
Fix CORS errors in API Gateway
Amazon API Gateway provides two API types with distinct CORS mechanics: REST APIs and HTTP APIs. REST APIs require explicit preflight handling through mock integrations or backend logic, while HTTP APIs use a built-in configuration object. When troubleshooting API Gateway CORS errors under Lambda proxy integration, the backend function itself must emit the headers directly.
REST APIs and HTTP APIs handle CORS differently
REST APIs have no central CORS toggle that covers every response path. The console Enable CORS action creates a mock OPTIONS method per resource and stamps headers onto method responses, while HTTP APIs have a built-in cors_configuration that answers preflights automatically.
| REST API | HTTP API | |
|---|---|---|
| Preflight handling | Mock OPTIONS integration per resource, created by Enable CORS or by hand | Automatic when cors_configuration is set |
| Response headers | Method and integration responses, or the Lambda handler under proxy integration | Added by API Gateway from the CORS configuration |
| Backend headers | Passed through; the backend can own them under proxy integration | Ignored once cors_configuration is set |
Lambda proxy integration: the handler owns the headers
A proxy integration returns no integration response, so API Gateway cannot stamp headers onto it. The Lambda function must return Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers on every response, including error responses. An unhandled exception produces a response without the headers, and the browser reports a CORS error that hides the real failure.
const CORS_HEADERS = {
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Origin': 'https://app.example.com',
'Access-Control-Allow-Methods': 'OPTIONS,POST,GET',
};
export const handler = async (event) => {
try {
const data = await loadData(event);
return { statusCode: 200, headers: CORS_HEADERS, body: JSON.stringify(data) };
} catch (error) {
// Error responses need the same headers or the browser
// reports a CORS error instead of the real status
return {
statusCode: 500,
headers: CORS_HEADERS,
body: JSON.stringify({ message: 'Internal error' }),
};
}
};Errors generated by API Gateway skip your integration
Responses that API Gateway generates itself, such as 403 Missing Authentication Token errors from failed authorization and throttling 429 responses, never reach the integration and carry no CORS headers by default. The browser then surfaces a CORS error instead of the real status code.
The fix is a gateway response configured at the API level. Add Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers header mappings to the default 4XX and default 5XX response types.
# API Gateway generates these itself: auth failures (403),
# throttling (429), unknown routes. They never touch your
# integration, so they carry no CORS headers by default.
#
# Console: API Gateway > your REST API > Gateway responses
# Add these header mappings to Default 4XX and Default 5XX:
#
# Access-Control-Allow-Origin 'https://app.example.com'
# Access-Control-Allow-Methods 'GET,POST,OPTIONS'
# Access-Control-Allow-Headers 'Content-Type,Authorization,X-Api-Key'HTTP APIs: built-in CORS configuration
With cors_configuration set, API Gateway answers preflight OPTIONS requests automatically even when no OPTIONS route exists, and adds the configured headers to the integration response. CORS headers returned by the backend are ignored.
# Allow one origin; API Gateway answers the preflight itself
aws apigatewayv2 update-api --api-id abc123def4 \
--cors-configuration AllowOrigins="https://app.example.com"
# The full property set: AllowOrigins, AllowMethods, AllowHeaders,
# ExposeHeaders, MaxAge, AllowCredentials.
# Note: headers returned by your backend are ignored once set.- The request must carry an
Originheader, and the OPTIONS probe must carryOriginandAccess-Control-Request-Method, or no CORS headers come back. - The
allowOriginsparameter accepts exact origins plus the wildcards*,https://*, andhttp://*. - With a
$defaultroute behind an authorizer, add anOPTIONS /{proxy+}route without authorization, or every preflight fails authentication because preflights carry no credentials.
Verify with curl
Probe both the actual request and the preflight. Then repeat with a bogus or missing token to confirm the error paths carry the header too.
BASE=https://abc123def4.execute-api.eu-west-1.amazonaws.com/prod
# Actual request: expect access-control-allow-origin
curl -i -H "Origin: https://app.example.com" $BASE/items
# Preflight: expect 2xx plus the allow headers
curl -i -X OPTIONS \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type" \
$BASE/items
# Error path: a bad token must still return the header on the 401/403
curl -i -H "Origin: https://app.example.com" \
-H "Authorization: Bearer invalid" $BASE/itemsA 401 or 403 without access-control-allow-origin points at the gateway responses, not the integration. Paste the response headers into the header checker to confirm what the browser will do with them.
Good questions.
Why does API Gateway show a CORS error on a 403 response?
API Gateway generates 403 Missing Authentication Token responses at the gateway layer when a route does not exist or authorization fails. These gateway responses carry no CORS headers by default, so the browser blocks the response and reports a CORS error instead of the 403.
Why are my Lambda CORS headers ignored on an HTTP API?
When cors_configuration is set on an HTTP API, API Gateway manages the CORS response headers itself and ignores any CORS headers returned by the backend integration.
Why does an authorizer break preflight requests on an HTTP API?
Browsers send no credentials on preflight OPTIONS requests. An authorizer on a $default route evaluates and rejects those preflights. Add an OPTIONS /{proxy+} route without authorization; it takes routing priority over the $default route.
What headers must a Lambda proxy integration return for CORS?
The handler must return Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers in the headers map of every response it emits, including error responses.