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.

CORS mechanics by API type
REST APIHTTP API
Preflight handlingMock OPTIONS integration per resource, created by Enable CORS or by handAutomatic when cors_configuration is set
Response headersMethod and integration responses, or the Lambda handler under proxy integrationAdded by API Gateway from the CORS configuration
Backend headersPassed through; the backend can own them under proxy integrationIgnored 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.

The handler returns the headers on every status code
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.

Gateway responses need the headers too
# 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.

HTTP APIs configure CORS once, centrally
# 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 Origin header, and the OPTIONS probe must carry Origin and Access-Control-Request-Method, or no CORS headers come back.
  • The allowOrigins parameter accepts exact origins plus the wildcards *, https://*, and http://*.
  • With a $default route behind an authorizer, add an OPTIONS /{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.

Probe the happy path and the error path
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/items

A 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.