Avoid pre-flight requests with AWS Application Load Balancer

János Krnák
uniplacesgeeks
Published in
5 min readSep 20, 2018

At Uniplaces we have a microservice architecture. If you are not familiar with microservices you can imagine it as having separate services around a domain or functionality. One of these services is the search API that powers our search pages and some other backend services.

When you go to www.uniplaces.com/accommodation/lisbon a JavaScript application loads which in the background fires a few XHR requests to the search API to fetch pins onto the map and to load cards of the offers on the left hand side.

What was our problem?

The search API is hosted on search-api.uniplaces.com, a different domain than www.uniplaces.com, where the search page lives. Because the two domains of the API and the web page are not the same and because we had some custom headers our requests were considered not simple requests and triggered the browser’s pre-flight requests. In other words, www was making a cross-origin request.

This meant that for every GET request the browser was sending an OPTIONS requests to ask the search API if the search page can make the request. This is not ideal, it slows down the user experience because you send out twice as many requests as you really should.

There is an OPTIONS pair for every GET

What is a pre-flight request?

CORS (Cross-Origin Resource Sharing) allows web pages to access content on another domain. For example, if you load www.example.com (www from now on) and it tries to send an XHR request to service.example.com (service form now on) the web browser won’t allow it because it’s not the same domain (simple requests can come into play here, more on that later).

Not all is lost though, service can configure CORS headers, that allows another origin to access its resources.

When the browser sees that www is about to access service it will send an OPTIONS request, in the response it sees that www.example.com is an allowed origin so it then makes the one of the GET,POST,PUT,DELETE request it was about to do at the first place. The methods that are allowed are coming back in the access-control-allow-methods response header.

If you’d like to understand CORS a bit deeper, I’d recommend reading MDN’s documentation about it.

You mentioned simple request

Yes I did. You can avoid pre-flight requests in some very limited scenarios which MDN deems as simple requests. Basically if you only send GET, POST or HEAD, no custom headers (see the headers allowed on the link above) and when the content-type is one of:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

This is very strict, imagine you are making POST requests with application/json body. You are out of luck, your request won’t fulfil the requirements for simple request.

How could we avoid the pre-flight requests?

You can cache the access control response for a URL with the access-control-max-age header. In this case only one OPTIONS request goes out to your URL as long as the cache of the access headers is fresh. This is maximum 10 minutes on Chrome and 24 hours on Firefox, but the catch is that each unique URL will need to be validated with a pre-flight request, as in, if you change query parameters, that’s a new URL. This way simply won’t scale for our API.

Another solution could be to move the custom headers to query parameters. For us this wasn’t an option, because even if we move the custom headers to query parameters, we need to remember next time, in weeks, in months that we are not allowed to add custom headers because that will trigger the pre-flight requests.

This option won’t scale on a fast paced growing team/company.

We had to find a solution that wouldn’t set us up for failure.

Let’s see why is this happening at the first place

Because of different origins. Because the search page and API has different hosts. So let’s put the API on the same domain as the search page!

We decided that we will have a route that will serve the requests to the search service. www.uniplaces.com/api/search/* will go to the search service!

Let’s proxy

We could go on a few different ways about it, set up a rule on the nginx web server to proxy the requests to the API, set up some in application proxying. Luckily a few months ago we migrated to AWS’s Application Load Balancer (ALB). ALB allows you to send requests matching a host (domain) and/or path pattern to a service (target group), or do a redirect, or send a fixed response…

What we needed is to send it to a service. We already had a rule set up for search-api.uniplaces.com to go to the search service. We had to do two things to make our idea into reality.

First: set up a rule that www.uniplaces.com/api/search/* should go to the search service.

The ALB rule based on path and host name

Second: as now we have a prefix (/api/search) on the requests going to the API we had to modify the service to accommodate this. We did it in a way to keep backward compatibility, we are are still serving the old routes but we server the same ones with the /api/search prefix also.

After the change, we don’t have the OPTIONS requests anymore as we stay on the same domain.

No more OPTIONS, same origin

Conclusion

If you would like to avoid the pre-flight requests, you have a few options but most of them is limited.

  • you can try to write all your requests as simple requests, but this quickly gets hacky
  • you can cache the CORS headers with access-control-max-age header, but this must be done for each unique URL, difficult to scale
  • you can proxy the requests through your domain

AWS’s Application Load Balancer is a very nice service. It allows you to set up routing based on hosts and/or paths, it allows you to redirect HTTP to HTTPS which is a common problem. They keep adding new and new features to it, it’s worth having a look if you have already bough into the AWS ecosystem.

--

--