Unlike simple requests, for "preflighted" requests the browser first sends an HTTP request using the OPTIONS
method to the resource on the other origin, in order to determine if the actual request is safe to send. Such cross-origin requests are preflighted since they may have implications for user data.
The following is an example of a request that will be preflighted:
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://bar.other/resources/post-here/');
xhr.setRequestHeader('X-PINGOTHER', 'pingpong');
xhr.setRequestHeader('Content-Type', 'text/xml');
xhr.onreadystatechange = handler;
xhr.send('<person><name>Arun</name></person>');
The example above creates an XML body to send with the POST
request. Also, a non-standard HTTP X-PINGOTHER
request header is set. Such headers are not part of HTTP/1.1, but are generally useful to web applications. Since the request uses a Content-Type
of text/xml
, and since a custom header is set, this request is preflighted.
Note: As described below, the actual POST
request does not include the Access-Control-Request-*
headers; they are needed only for the OPTIONS
request.
Let's look at the full exchange between client and server. The first exchange is the preflight request/response:
OPTIONS /doc HTTP/1.1
HTTP/1.1 204 No Content
Lines 1 - 10 above represent the preflight request with the OPTIONS
method. The browser determines that it needs to send this based on the request parameters that the JavaScript code snippet above was using, so that the server can respond whether it is acceptable to send the request with the actual request parameters. OPTIONS is an HTTP/1.1 method that is used to determine further information from servers, and is a safe method, meaning that it can't be used to change the resource. Note that along with the OPTIONS request, two other request headers are sent (lines 9 and 10 respectively):
The Access-Control-Request-Method
header notifies the server as part of a preflight request that when the actual request is sent, it will do so with a POST
request method. The Access-Control-Request-Headers
header notifies the server that when the actual request is sent, it will do so with X-PINGOTHER
and Content-Type
custom headers. Now the server has an opportunity to determine whether it can accept a request under these conditions.
Lines 12 - 21 above are the response that the server returns, which indicate that the request method (POST
) and request headers (X-PINGOTHER
) are acceptable. Let's have a closer look at lines 15-18:
The server responds with Access-Control-Allow-Origin: https://foo.example
, restricting access to the requesting origin domain only. It also responds with Access-Control-Allow-Methods
, which says that POST
and GET
are valid methods to query the resource in question (this header is similar to the Allow
response header, but used strictly within the context of access control).
The server also sends Access-Control-Allow-Headers
with a value of "X-PINGOTHER, Content-Type
", confirming that these are permitted headers to be used with the actual request. Like Access-Control-Allow-Methods
, Access-Control-Allow-Headers
is a comma-separated list of acceptable headers.
Finally, Access-Control-Max-Age
gives the value in seconds for how long the response to the preflight request can be cached without sending another preflight request. The default value is 5 seconds. In the present case, the max age is 86400 seconds (= 24 hours). Note that each browser has a maximum internal value that takes precedence when the Access-Control-Max-Age
exceeds it.
Once the preflight request is complete, the real request is sent:
POST /doc HTTP/1.1
<person><name>Arun</name></person>
HTTP/1.1 200 OK
[Some XML payload]
Preflighted requests and redirects
Not all browsers currently support following redirects after a preflighted request. If a redirect occurs after such a request, some browsers currently will report an error message such as the following:
The request was redirected to 'https://example.com/foo', which is disallowed for cross-origin requests that require preflight. Request requires preflight, which is disallowed to follow cross-origin redirects.
The CORS protocol originally required that behavior but was subsequently changed to no longer require it. However, not all browsers have implemented the change, and thus still exhibit the originally required behavior.
Until browsers catch up with the spec, you may be able to work around this limitation by doing one or both of the following:
- Change the server-side behavior to avoid the preflight and/or to avoid the redirect
- Change the request such that it is a simple request that doesn't cause a preflight
If that's not possible, then another way is to:
- Make a simple request (using
Response.url
for the Fetch API, or XMLHttpRequest.responseURL
) to determine what URL the real preflighted request would end up at. - Make another request (the real request) using the URL you obtained from
Response.url
or XMLHttpRequest.responseURL
in the first step.
However, if the request is one that triggers a preflight due to the presence of the Authorization
header in the request, you won't be able to work around the limitation using the steps above. And you won't be able to work around it at all unless you have control over the server the request is being made to.