1. Introduction
Sometimes HTTP requests, triggered from JavaScript on our webpage, are preceded by an OPTIONS request.
In this tutorial, we’ll look at why this additional request is sent.
Next, we’ll take a look at how to suppress this so-called preflight request. In those cases where suppressing isn’t an option, we’ll take a look at how we can limit them.
2. Background
The OPTIONS request mentioned in the introduction is a preflight request, which is part of the CORS (Cross-Origin Resource Sharing). CORS is a mechanism that provides configuration to configure access to shared resources. CORS applies when a webpage makes a request to another server other than its origin server, this could mean that either the domain, protocol, or port differs.
Using the request the browser checks with the server whether the request is allowed. Only if the request is allowed, it’ll actually perform it.
Before performing a PUT request to www.baeldung.com/demo from foo.baeldung.com a preflight request would be performed, it can look as follows:
OPTIONS /demo HTTP/1.1
Host: www.baeldung.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36
Accept: */*
Accept-language: nl-NL,nl;q=0.9,en-US;q=0.8,en;q=0.7
Connection: keep-alive
Origin: http://foo.baeldung.com
Access-Control-Request-Method: PUT
Note, that even for CORS requests without a preflight request, it’s still required that the server allows the cross-origin request, for this, the browser requires an appropriate Access-Control-Allow-Origin header in the server’s response.
3. Request Types
CORS distinguishes two different types of requests. The official specification doesn’t give requests without a preflight a name, however, MDN uses the name “simple requests”, which we’ll use here as well. We’ll also use a slight simplification of the rules as mentioned in the CORS specification. The specification contains additional constraints on the allowed characters in the values, to further enhance security.
When a request is either a GET, POST, or a HEAD request it can be eligible to be a simple request. Besides that, the request is only allowed to include headers from the safelist, consisting of the following headers:
- Accept
- Accept-Language
- Content-Language
- Content-Type
For the Content-Type header, only the following limited set of values is allowed:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
Finally, there should be no usage of event listeners on the XMLHttpRequestUpload object or usage of a ReadableStream in the request.
All other requests will only be executed after the response of the preflight request confirms that the request is allowed.
4. Disabling the OPTIONS Request
Let’s take a look at our options to disable the additional OPTIONS request.
4.1. Make It a Same-Origin Request
Let’s first validate whether it is intentional that the calls we perform are going to a different server? Maybe there is something else going on. Did we make a mistake setting up our environment? Did we unintentionally call our API over a different protocol, e.g. are we using HTTP-calls on our HTTPS website? Okay, these were the most essential checks. What else can we do?
We can consider proxying API requests such that they go through the origin server. Or we can consider making both our website and the API available under different paths on the same hostname, using a reverse-proxy server. Okay, if we are sure that we really need a cross-origin request, let’s look at some other options in the next section.
4.2. Make It a Simple Request
In one of the previous sections, we learned that a preflight request isn’t sent for simple requests. So if we want to disable the preflight request, our next best option is to make sure that the request is a simple request. This assumes that the server sends the proper Access-Control-Allow-Origin header.
So that means, we can perform a GET request without the need for a preflight request. However, the restrictions for POST requests are tighter. It means, for example, that we cannot send a JSON request without a preflight. However, sending a ‘form’ is fine.
Thus, the following JavaScript fragment will not trigger a preflight request:
var request = new XMLHttpRequest();
request.open("POST", 'http://localhost:8080/with-valid-cors-header');
request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
request.send("a=b&c=d");
However, when we would change the request method, add additional headers, or change the content-type we’ll end up with a request that requires an additional OPTIONS request.
Finally, even when a request requires a preflight, we can limit the number of preflights. We do this by instructing the browser to cache the CORS configuration, in the next section we’ll go into more details about this.
5. Caching the OPTIONS Request
Sometimes, we’re not able to make the request a same-origin request or a simple request. In that case, we can make sure that we at least limit the number of preflight requests. Let’s do this by setting a max-age on the preflight response.
The header Access-Control-Max-Age is responsible for caching the CORS configuration. The value of this header is expressed in seconds. An example allowing caching for 5 minutes would look as follows:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, PUT, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 300
More background on the other headers can be found in MDN’s CORS article.
6. Conclusion
In this article, we looked at why some requests are preceded by an OPTIONS request. We briefly explained CORS and the rules determining whether a request will be pre-flighted. Next, we looked at some tips and tricks to make sure we are not making an unnecessary preflight request. Finally, we looked at options to cache our CORS configuration.