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 incoming Origin header 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 sends Authorization or Content-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 the Authorization header 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

  1. 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.
  2. 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.
  3. The Mysterious Missing Header: You added Authorization to your Access-Control-Allow-Headers list, 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.
  4. 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.