12.6 CORS Configuration for Browser-Facing APIs
Alright, let’s talk about CORS. You’re going to hate it. I hate it. We all hate it. But you know what we hate more? Our web apps not working because some browser security model we didn’t fully understand decided to block our requests. CORS, or Cross-Origin Resource Sharing, is that security model. It’s not an API Gateway feature; it’s a browser feature. API Gateway just gives you the knobs to respond to the browser’s interrogation correctly.
Think of it this way: your browser is an overly paranoid bouncer. Your web app, running on https://myapp.com, tries to call your API on https://myapi.com. The bouncer (the browser) says, “Whoa there, hotshot. I don’t care if you own that other club. I haven’t heard from the manager (https://myapi.com) that you’re allowed to come in.” So it blocks the request. To get in, your web app must first send a “preflight” request—the bouncer sending a scout ahead to ask the manager for the rules. The manager (your API) must then respond with a set of headers explicitly saying, “Yes, https://myapp.com is allowed to make these types of requests.” That entire conversation is CORS.
The Preflight (OPTIONS) Request
This is the most common “WTF?” moment for developers new to CORS. When your browser-based JavaScript makes a “non-simple” request (anything beyond basic GET/POST with standard headers), the browser first sends an HTTP OPTIONS request to the API. This is the scout. Your API must respond correctly to this OPTIONS request, or the actual request gets discarded.
API Gateway handles this by letting you mock an OPTIONS response. You set up the CORS headers for the OPTIONS method, and Gateway sends them back, satisfying the browser’s scout. Here’s the kicker: you have to enable CORS for both the OPTIONS method and your actual methods (like GET, POST). Forgetting the latter is a classic blunder.
Configuring CORS in API Gateway
In the API Gateway console, there’s a big “Enable CORS” button. It’s tempting. Don’t. It only works for simple cases and will likely break your setup. It often adds a wildcard (*) for Access-Control-Allow-Origin, which is a bad practice if your API uses credentials (cookies, auth headers). Instead, do it manually. It’s clearer and more powerful.
You configure CORS by adding the required headers to the method response and integration response of your OPTIONS method and your actual methods.
Let’s build a proper OPTIONS response for a POST endpoint. First, define the method response headers for your OPTIONS method:
# SAM Template example for the OPTIONS method response
OptionsMethod:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref MyApi
ResourceId: !Ref MyResource
HttpMethod: OPTIONS
AuthorizationType: NONE
MethodResponses:
- StatusCode: 200
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: true
method.response.header.Access-Control-Allow-Methods: true
method.response.header.Access-Control-Allow-Origin: true
Now, the integration that will return those headers. We’ll use a mock integration:
# Integration response for the OPTIONS method
Integration:
Type: MOCK
RequestTemplates:
application/json: '{"statusCode": 200}'
IntegrationResponses:
- StatusCode: 200
ResponseParameters:
integration.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'"
integration.response.header.Access-Control-Allow-Methods: "'OPTIONS,POST'"
integration.response.header.Access-Control-Allow-Origin: "'https://mytrustedapp.com'"
ResponseTemplates:
application/json: ''
See how we’re mapping the integration response headers to the method response headers? That’s the crucial link.
The Critical Headers and What They Mean
Access-Control-Allow-Origin: This is the big one. It specifies which origin is allowed. It can be a single origin ('https://myapp.com') or a wildcard ('*'). Crucially, you cannot use a wildcard if you’re using credentials like cookies or authorization headers. The browser will reject it. If you need multiple origins, you have to check the incomingOriginheader in a Lambda function and echo back the valid one. It’s a pain.Access-Control-Allow-Methods: A comma-separated list of HTTP methods you’re allowing.'GET,POST,OPTIONS'. Don’t just use'*'here. Be specific. It’s more secure.Access-Control-Allow-Headers: This tells the browser which headers the client is allowed to send. If your frontend sendsAuthorizationorContent-Type, they must be listed here. If you see errors about a header not being allowed, this is the culprit. API Gateway, in its infinite wisdom, gets fussy about theAuthorizationheader if you don’t explicitly list it here.
Doing it Right with Lambda Integration
If you’re using a Lambda function for your main request (POST, GET), you also need to add the CORS headers to its response. The OPTIONS request is handled by the mock above, but the response to your actual POST needs headers too, or the browser will still block it.
Here’s how you’d do it in your Lambda function’s response:
// Lambda function response for a POST request
exports.handler = async (event) => {
// ... your logic here ...
const response = {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': 'https://mytrustedapp.com', // MUST match the OPTIONS response
'Access-Control-Allow-Credentials': true, // Required if using cookies/auth
'Content-Type': 'application/json'
},
body: JSON.stringify({ message: "Success!" }),
};
return response;
};
Notice Access-Control-Allow-Credentials: true. You need this if you’re sending cookies or authentication headers. And remember, if you use this, your Access-Control-Allow-Origin must be a specific origin, not a wildcard.
Common Pitfalls and Debugging
- The 403 Forbidden on Preflight: This almost always means you haven’t deployed your API after making CORS changes. CORS configuration is part of your API’s configuration state. Change it, you must redeploy. Every. Single. Time.
- The 200 OK but Still Blocked: Your preflight (OPTIONS) worked, but your actual method (POST) is missing the CORS headers. Check your Lambda function’s response or your integration response for the main method.
- The Mysterious Missing Header: You added
Authorizationto yourAccess-Control-Allow-Headerslist, but it’s still blocked. API Gateway can be finicky. Try listing it as'Authorization'instead of'authorization'(case matters sometimes), or add an extra'X-Amz-Date'for good measure. It’s absurd, but it often works. - Using a Wildcard with Credentials: The browser will give you a cryptic error. Just remember: credentials +
Allow-Origin: *= failure. Pick one.
Debugging CORS is best done with your browser’s DevTools open. Look at the Network tab. You’ll see the OPTIONS request. Click on it and see what headers are actually coming back. 99% of the time, the answer is right there, staring back at you, wrong. Fix the headers, redeploy, and try again. Welcome to the club.