Frontend applications executing cross-origin HTTP requests frequently encounter the dreaded No Access-Control-Allow-Origin error in the browser console. When interacting with an AWS Lambda REST API, this error introduces a unique layer of complexity.
Developers often configure CORS in the AWS Management Console for API Gateway, deploy the API, and still encounter the exact same browser blockage. This occurs because the architectural relationship between API Gateway and Lambda requires a specific, two-tiered approach to header management.
Understanding the Root Cause of AWS API Gateway CORS Failures
Browsers enforce the Same-Origin Policy (SOP) to prevent malicious scripts on one origin from accessing data on another. When your frontend (e.g., https://app.example.com) requests data from your backend (https://api.example.com), the browser initiates a Cross-Origin Resource Sharing (CORS) check.
For mutating requests (POST, PUT, DELETE) or requests with custom headers, the browser first sends an HTTP OPTIONS request, known as a preflight. The server must respond to this preflight with specific headers explicitly permitting the origin, methods, and headers.
The disconnect in AWS environments stems from the AWS Lambda Proxy Integration. When proxy integration is enabled, API Gateway passes the entire raw HTTP request directly to the Lambda function. Consequently, API Gateway delegates the responsibility of constructing the HTTP response—including the CORS headers for the actual GET or POST request—entirely to the Lambda function. If you configure CORS on the API Gateway but forget to include the headers in your Lambda return object, the browser drops the response.
The Fix: Aligning Infrastructure and Application Layers
To completely fix No Access-Control-Allow-Origin errors, you must address both the preflight request at the infrastructure layer and the actual response at the application layer.
1. The Application Layer (AWS Lambda Node.js)
When using Lambda Proxy Integration, your Lambda function's return object must explicitly contain the Access-Control-Allow-Origin header. Without this, the preflight may succeed, but the subsequent main request will fail.
Here is a production-ready Node.js Lambda handler utilizing ES modules. This implementation correctly formats the response object required by API Gateway.
import { randomUUID } from 'crypto';
export const handler = async (event) => {
// Extract origin for dynamic CORS handling (required for credentialed requests)
const origin = event.headers.origin || event.headers.Origin || '';
const allowedOrigins = ['https://app.example.com', 'http://localhost:3000'];
const corsOrigin = allowedOrigins.includes(origin) ? origin : allowedOrigins[0];
const headers = {
'Access-Control-Allow-Origin': corsOrigin,
'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
'Access-Control-Allow-Methods': 'OPTIONS,POST,GET,PUT,DELETE',
'Access-Control-Allow-Credentials': 'true',
'Content-Type': 'application/json'
};
try {
// Process the incoming request body
const body = event.body ? JSON.parse(event.body) : {};
const responsePayload = {
message: "Data successfully processed.",
requestId: randomUUID(),
receivedData: body
};
return {
statusCode: 200,
headers: headers,
body: JSON.stringify(responsePayload),
};
} catch (error) {
console.error('Processing Error:', error);
return {
statusCode: 500,
headers: headers, // Critical: Include headers even on error responses
body: JSON.stringify({ message: "Internal Server Error" }),
};
}
};
2. The Infrastructure Layer (AWS CDK)
Manual configuration via the AWS Console is prone to drift. Managing your cloud infrastructure deployment through Code ensures consistent API Gateway CORS behavior.
Using the AWS Cloud Development Kit (CDK) in TypeScript, you can configure the API Gateway to automatically mock the OPTIONS preflight responses, decoupling that specific burden from your Lambda function.
import * as cdk from 'aws-cdk-lib';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
export class ApiStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Define the Lambda function
const backendLambda = new lambda.Function(this, 'BackendHandler', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
});
// Define the AWS Lambda REST API with global CORS settings for OPTIONS requests
const api = new apigateway.RestApi(this, 'CorsConfiguredApi', {
restApiName: 'Backend Service',
defaultCorsPreflightOptions: {
allowOrigins: ['https://app.example.com', 'http://localhost:3000'],
allowMethods: apigateway.Cors.ALL_METHODS,
allowHeaders: ['Content-Type', 'Authorization', 'X-Api-Key'],
allowCredentials: true,
},
});
// Attach Lambda Proxy Integration
const lambdaIntegration = new apigateway.LambdaIntegration(backendLambda);
// Add a resource and method
const items = api.root.addResource('items');
items.addMethod('POST', lambdaIntegration);
}
}
Deep Dive: Why This Implementation Works
The architecture above creates a strict boundary of responsibility. The API Gateway handles the OPTIONS request autonomously via the defaultCorsPreflightOptions configuration. It intercepts the browser's preflight and returns an HTTP 200 with the configured headers without ever invoking the Lambda function. This saves execution time and reduces AWS billing costs.
When the browser sends the actual POST request, API Gateway passes the payload to Lambda. Because we are using Proxy Integration, API Gateway becomes a passive conduit for the response. The Node.js Lambda code constructs the HTTP response object explicitly containing the Access-Control-Allow-Origin header.
By dynamically checking the incoming event.headers.origin against an allowed list, the Lambda function safely supports multiple environments (like staging and production) while adhering to strict security policies.
Common Pitfalls and Edge Cases
The 5xx Error Masking
A frequent source of confusion occurs when a backend error masquerades as a CORS error. If your Lambda function throws an unhandled exception or times out, API Gateway generates a default 502 Bad Gateway response. Because this default error response bypasses your Lambda function's code, it lacks the necessary CORS headers. The browser intercepts this missing header and reports a CORS error, obscuring the actual backend crash. Always include CORS headers in your catch blocks and monitor Amazon CloudWatch logs to verify if a Lambda crash is the true culprit.
Allow-Credentials and Wildcards
If your frontend application uses cookies, authorization headers, or TLS client certificates, you must configure your HTTP client (like fetch or axios) with credentials: 'include'. Under the HTTP specification, if credentials are included, the Access-Control-Allow-Origin header cannot be a wildcard (*). It must be the explicit origin. The dynamic origin matching in the Lambda Node.js code provided above resolves this constraint by echoing the specific requesting origin.
REST APIs vs HTTP APIs (API Gateway v2)
AWS offers two types of API Gateways: REST APIs (v1) and HTTP APIs (v2). The solution provided targets REST APIs, which are the most common and feature-rich. If you are deploying an HTTP API, the behavior differs. HTTP APIs can be configured to natively append CORS headers to all responses, including Lambda proxy integrations, rendering manual header inclusion inside the Lambda code unnecessary. Always verify which API Gateway version your infrastructure relies on before debugging.