How CORS works on AWS
A browser lets a page read a response from another origin - another scheme, host or port - only if the response says so with Access-Control-Allow-Origin. Requests with JSON, custom headers such as Authorization, or methods other than GET, HEAD and POST first send a preflight OPTIONS request, and the real request follows only if the preflight response allows its origin, method and headers. CORS is enforced by the browser alone: curl, Postman and server-side code never see a CORS error, which is why "it works in Postman" says nothing about it.
Each AWS service that answers browser requests configures CORS in its own way:
| Service | Where CORS is set | Watch out for |
|---|---|---|
| API Gateway REST API | An OPTIONS method, plus headers in every response - from the Lambda function with a proxy integration | "Enable CORS" does not change what a Lambda proxy returns; deploy after every change; API Gateway's own errors need gateway responses |
| API Gateway HTTP API | The API's CORS configuration | Backend CORS headers are ignored once it is set; an authorizer on $default or ANY catches OPTIONS |
| Lambda function URL | The function URL's CORS configuration | Headers from the code are added on top: duplicate Access-Control-Allow-Origin |
| Amazon S3 | The bucket's CORS configuration (up to 100 rules) | Methods only GET, PUT, POST, DELETE, HEAD; no match means no CORS headers at all |
| CloudFront | A response headers policy, or the origin's own headers with Origin forwarded | Responses cached without the Origin header; OPTIONS must be an allowed method |
CORS errors are often other errors
When a request fails, the error response usually carries no CORS headers - API Gateway's Missing Authentication Token, an authorizer's 401, a Lambda function's crash, an S3 access denial. The browser then reports "No 'Access-Control-Allow-Origin' header" and hides the real error. Before changing any CORS setting, look at the status code in the Network tab of the developer tools, or send the request without a browser:
curl -i -X OPTIONS https://abc123defg.execute-api.us-east-1.amazonaws.com/prod/items \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type,authorization" A 2xx answer with Access-Control-Allow-* headers means the preflight passes; then repeat it with the real method. Paste the browser's message into the tool above: it reads the status code, the origin and the rejected header or method from it.
| Error | Most common cause on AWS |
|---|---|
| No 'Access-Control-Allow-Origin' header is present | The request failed (look at the status), or a Lambda proxy integration does not return the header |
| Response to preflight request doesn't pass access control check: It does not have HTTP ok status | An authorizer or API key on OPTIONS, or no OPTIONS method |
| Request header field ... is not allowed by Access-Control-Allow-Headers | The header (often Authorization or x-api-key) is missing from the allowed headers |
| The 'Access-Control-Allow-Origin' header contains multiple values | Both a function URL or API Gateway and the code add the header |
| must not be the wildcard '*' when the request's credentials mode is 'include' | Cookies are sent; list the origins and allow credentials |
| Missing Authentication Token | Wrong path, method or stage in a REST API, or the API was not deployed |
| Internal server error (502) | The Lambda function failed or returned a malformed proxy response |
The Lambda proxy response format
With a Lambda proxy integration, API Gateway passes the function's return value to the browser, and it has to have this shape - anything else is a 502 Malformed Lambda proxy response:
{
"statusCode": 200,
"headers": {"Access-Control-Allow-Origin": "https://app.example.com", "Content-Type": "application/json"},
"body": "{\"message\":\"Hello\"}",
"isBase64Encoded": false
}body is a string - the result of JSON.stringify or json.dumps, not the object itself - and every response, errors included, needs the CORS headers. Paste what your function returns into the tool to check it.
Frequently asked questions
Why does my API work in Postman or curl but not in the browser?
Only browsers enforce CORS. Postman and curl send the request and show the response whatever its headers say; the browser sends the same request and then refuses to give the response to your page.
I enabled CORS in API Gateway and still get the error. Why?
For a REST API with a Lambda proxy integration, "Enable CORS" only creates the OPTIONS method - the function has to return Access-Control-Allow-Origin itself. The change also takes effect only after you deploy the API. And if the request fails (401, 403, 502), the error response has no CORS headers unless gateway responses add them.
Can I allow several origins?
Access-Control-Allow-Origin holds one origin or *. S3, HTTP APIs, function URLs and CloudFront take a list and answer with the matching one; in Lambda code, compare the request's Origin with your list, return it when it matches and add Vary: Origin - the generated code does.
Is it safe to paste my error here?
Yes. Everything runs in your browser: nothing you enter is uploaded, processed on a server or stored. The page only counts that it was used and which error it recognized, never the text.
References
CORS for REST APIs in API Gateway
Configure CORS for HTTP APIs in API Gateway
CORS for Lambda function URLs
Elements of an S3 CORS configuration
CloudFront response headers policies: CORS headers
Output format of a Lambda function for proxy integration