Fixing up CORS errors in HTTP API

When we create a single route for multiple request types, we might receive a CORS error from browser requests. It's easy to generate a mock integration for preflight requests in our HTTP API using Lambda functions.

1. Problem statement

Alice has been building an new service using an HTTP API Gateway. A React application will call the endpoints, and she will use tokens in the Authorization header to authorize the requests with API Gateway.

She configured a custom domain and set up CORS in the API Gateway because most requests will come from the browser.

All went well until she added the ANY /{proxy+} catch-all route. After she had added the authorizer to it and invoked the endpoint with the GET /todos request, she received the following error:

Access to XMLHttpRequest at 'https://SUBDOMAIN.DOMAIN_NAME/todos' from
origin 'http://localhost:3000' has been blocked by CORS policy: Response
to preflight request doesn't pass access control check: It does not have
HTTP ok status.

What can she do now?

2. Options

The CORS configuration she set up in API Gateway looks like this:

HTTP API CORS configuration
HTTP API CORS configuration

It allows requests from localhost:3000 for front-end development. The configuration contains the HTTP request methods the application will use. The allowed headers look good, too.

The endpoint still responds with the above CORS error.

By default, API Gateway will automatically send a response to preflight OPTIONS requests. As long as we specify the method in the route, e.g., GET /todos or POST /todos, we won’t have any CORS issues. But when we use ANY in the route, we will receive the error.

If we leave the endpoint unprotected and remove the authorizer, we will have a successful request. The problem is that we should protect the route so only authenticated users can call it.

3. A solution

Instead, we can create an unauthorized OPTIONS route with custom integration.

We can add the original integration to OPTIONS, but it’s better to consider a mock integration.

REST API Gateways support mock integrations, which we can use for the OPTIONS routes. Unfortunately, we don’t have built-in mock integrations for HTTP APIs.

It means that we have to create one. It’s easy to do so, and a simple Lambda function will do the job:

exports.handler = async (event) => {
  const response = {
    statusCode: 204,
  };
  return response;
};

We can attach this function to the OPTIONS as an integration.

4. ANY routes

It’s not just the ANY /{proxy+} route that fails with the CORS error but other ANY routes, too. For example, the ANY /todos endpoint will also return the CORS error if we protect it with an authorizer.

The solution here is the same as above, create an OPTIONS route and add the preflight handler Lambda function as an integration. We can reuse the same function for multiple ANY routes.

5. The API settings will play

If we hit the ANY endpoint with the https://SUBDOMAIN.DOMAIN_NAME/todos request from the browser application, the response headers will contain the parameters we have set in the CORS section of the API Gateway.

Preflight response headers
Preflight response headers

It is because API Gateway will ignore the integration response. The preflight handler here only returns a 204 status code, which will be the response’s HTTP status. Everything else will come from the settings we specified for CORS.

6. Summary

Catch-all routes will respond with a CORS error unless we specify an unauthorized OPTIONS endpoint. Because this endpoint is unprotected, we should attach a mock integration to this route. HTTP API doesn’t have the concept of mock integrations, so it’s worth creating one and using that for the OPTIONS request.

7. Further reading

Choosing between REST APIs and HTTP APIs - Comparison with lots of tables

Configuring CORS for an HTTP API - It is exactly what the title says

Cross-Origin Resource Sharing (CORS) - Overview of CORS