Handling preflight requests - CORS on the server - CORS in Action: Creating and consuming cross-origin APIs (2015)

CORS in Action: Creating and consuming cross-origin APIs (2015)

Part 2. CORS on the server

Chapter 4. Handling preflight requests

This chapter covers

· What a CORS preflight is

· How to respond to a CORS preflight

· How the preflight cache works

The previous chapter showed how to respond to CORS requests by using the Access-Control-Allow-Origin header. While this header is required on all valid CORS responses, there are some cases where the Access-Control-Allow-Origin header alone isn’t enough. Certain types of requests, such as DELETE or PUT, need to go a step further and ask for the server’s permission before making the actual request.

The browser asks for permissions by using what is called a preflight request. A preflight request is a small request that is sent by the browser before the actual request. It contains information like which HTTP method is used, as well as if any custom HTTP headers are present. The preflight gives the server a chance to examine what the actual request will look like before it’s made. The server can then indicate whether the browser should send the actual request, or return an error to the client without sending the request.

This chapter will examine what a preflight request is and when it’s used. Next it will introduce headers the server can use to respond to a preflight. It will then introduce the preflight cache, which is a browser optimization that helps limit the number of preflight requests that are made.

4.1. What is a preflight request?

Let’s think about a preflight request in the context of the ATM example from chapter 3. Banks sometimes put their ATMs inside a room behind a locked door. The door can only be unlocked by swiping your ATM card (or if a kind person lets you in, but let’s ignore that for now). Once you’re inside, you can walk up to the ATM and withdraw money. The simple act of swiping your card to unlock the door doesn’t automatically give you money, but it’s a quick check to verify that you have permission to use the ATM.

In a similar fashion, a preflight request asks for the server’s permission to send the request. The preflight isn’t the request itself. Instead, it contains metadata about it, such as which HTTP method is used and if the client added additional request headers. The server inspects this metadata to decide whether the browser is allowed to send the request.

By asking for permission before making the request, the preflight introduces an additional processing step to CORS. Let’s dig deeper into how this new step fits into your existing understanding of CORS.

4.1.1. Lifecycle of a preflight request

chapter 3 framed CORS requests in the context of a conversation between the browser and the server. The preflight augments this conversation with additional dialogue, as shown in Figure 4.1. This conversation is a bit longer than the conversation from chapter 3. It adds the first two lines, where the browser asks the server for permission to use the DELETE method. These two lines are the preflight request, while the last two lines are the CORS request.

Figure 4.1. Preflight versus actual request

Figure 4.2 expands Figure 3.12 in chapter 3 to show how the preflight request fits into the lifecycle of a CORS request. The browser uses the server’s response to the preflight to determine if the request can be made. If the server grants the right permissions on the preflight response, the browser sends the request to the server. The server can also decide not to approve the request, in which case the browser will return an error to the client, and the request will never be sent.

Figure 4.2. Lifecycle of a CORS request (with preflight)

Now that you have a sense of what a preflight request is, let’s discuss why it exists in the first place.

4.1.2. Why does the preflight request exist?

The concept of a preflight was introduced to allow cross-origin requests to be made without breaking existing servers that depend on the browser’s same-origin policy. If the preflight hits a server that is CORS-enabled, the server knows what a preflight request is and can respond appropriately. But if the preflight hits a server that doesn’t know or doesn’t care about CORS, the server won’t send the correct preflight response, and the actual request will never be sent. The preflight protects unsuspecting servers from receiving cross-origin requests they may not want.

This is best conveyed by a story. Imagine it is is 2004. The web is still young, the term Web 2.0 was only recently coined, and you’re the administrator of a small news site. Much like the sample app, this site usesXMLHttpRequests to load news data from an API, as shown in figure 4.3. Because your site lives under the same origin as your API, it can make any type of HTTP request to the API.

Figure 4.3. Your server, circa 2004. CORS doesn’t yet exist, but your site and your API live under the same origin, so they can communicate.

Not only does this API fetch news stories, but it also lets you, the owner, edit and delete news items. While you have basic security measures in place, your code never checks which origin a request is coming from, because why should it? All browsers enforce the same-origin policy. There is no such thing as CORS (remember this is 2004), so there is no way for clients from other origins to access your API.

Now fast-forward to 2009. Your news site has become much more popular, and it’s still humming along nicely, thanks to the clean architectural separation between your frontend and the API. But then late one night you read that Chrome 4.0 will be released soon, and it supports this new feature called Cross-Origin Resource Sharing that allows cross-origin requests.

You find this a bit troubling, and that night you have nightmares of your server being deluged with all sorts of requests from servers across the web, as shown in figure 4.4. You wake in a cold sweat wondering how you’ll protect your server from these cross-origin requests. Will users suddenly be able to send DELETE requests without your permission? Why would browsers suddenly break the same-origin policy you have come to rely on?

Figure 4.4. A CORS nightmare scenario. Cross-origin requests are made without any permission from the server. Thankfully this isn’t how CORS is implemented.

Luckily, CORS answers these questions. The arrival of CORS didn’t cause thousands of server administrators to wake in a cold sweat. In fact, browser support for CORS was a fairly painless rollout because when the CORS spec was being drafted, the spec authors recognized that CORS needed to be introduced in a way that was compatible with existing servers.

The answer to preserving backward compatibility was to introduce the preflight request. The preflight request is a way for the browser to ask the server if it’s okay to send a cross-origin request before sending the actual request. The same-origin policy is still preserved, because the request is never made unless the server grants permission. An existing server that knows nothing about CORS can safely ignore the preflight request, and the browser will not forward the actual request to the server, as shown in Figure 4.5.

Figure 4.5. The CORS preflight request prevents unauthorized API requests from ever reaching your server.

To return to the story, after learning about the CORS preflight request, you rest a little easier that night, knowing that your server won’t receive any unauthorized requests from other people’s servers.

This story demonstrates why the preflight was introduced: it allows cross-origin requests to be introduced to the web in a way that doesn’t adversely affect existing servers. Now that you know why the preflight request exists, let’s modify the sample code to trigger a preflight request.

4.2. Triggering a preflight request

chapter 3 introduced a sample blogging app that loads blog posts using CORS. Let’s modify that to let the user delete blog posts. The standard method for deleting data in a REST API is to use the HTTP DELETE method, so we’ll use that here.

Listing 4.1 modifies client.html to display a Delete link next to each blog post. Clicking the link calls the deletePost JavaScript function. The deletePost function deletes a blog post by sending a DELETE request to the URL /api/posts/{ID}, where {ID} is the blog post’s ID. For example, a request to /api/posts/1 would delete the blog post with an ID of 1. If the delete is successful, the post is removed from the page.

Listing 4.1. Adding a function to delete posts

This listing modifies the client code. Now let’s turn our attention to the server code to respond to the DELETE request. Listing 4.2 modifies the app.js server code to handle the incoming DELETE request. It listens for DELETE requests on the /api/posts/{ID} URL. When it receives a DELETE request, the code deletes the post with the corresponding ID and returns the HTTP 204 status code. HTTP 204 means that the request was successful, but the body has no content; it’s the traditional response code used for DELETE requests in REST APIs.

Listing 4.2. Adding server support for deleting posts

After making the changes in listings 4.1 and listings 4.2, restart the server and reload the client.html page. You should see a Delete link next to each blog post, as shown in Figure 4.6.

Figure 4.6. Adding a Delete link next to each blog post

The code looks like it should work, because you added CORS support to your server in the previous chapter. But if you click a Delete link, you’ll see the error message shown in Figure 4.7. What is going on here?

Figure 4.7. Error message when trying to delete a post

Looking at the error in figure 4.7, one inconsistency should stand out: although the code is making a DELETE request , the error is on an OPTIONS request . The browser never sends the DELETE request. This certainly looks weird, but this isn’t a bug. What you’re seeing is the preflight request.

Seeing the preflight on the DELETE request may lead you to ask, why didn’t the GET request to load posts also have a preflight? The preflight request is only sent some of the time. The next section looks at when a preflight request is sent.

4.2.1. When is a preflight request sent?

Even with the same-origin policy in place, there are ways to make some types of cross-origin requests from the browser. Appendix D goes into more details about these techniques, but here are two ways the client can circumvent the same-origin policy:

· A web page can easily make GET requests to another origin. Every <script> tag or <img> tag issues a GET request.

· A web page can make POST requests via the <form> tag. The <form> tag also allows the Content-Type header to be set to application/x-www-form-urlencoded, multipart/form-data, or text/plain.

The following listing shows the code that can be used to make these cross-origin requests. (You don’t need to download or run this listing, it’s only meant as an example.)

Listing 4.3. Making cross-origin requests without CORS

These techniques existed long before the concept of CORS and preflights. Because the browser can make these requests without CORS, the preflight doesn’t provide any additional value. For example, if there were a preflight on aGET request, the client could always use a script tag to get around the preflight. The browser skips the preflight in cases where the client can already make the cross-origin request through other means.

This gives a general overview of when a preflight is used. To state it more concretely, a preflight request is issued when a request meets any of the following criteria:

· It uses an HTTP method other than GET, POST, or HEAD

· It sets the Content-Type request header with values other than

1. application/x-www-form-urlencoded

2. multipart/form-data

3. text/plain

· It sets additional request headers that are not

1. Accept

2. Accept-Language

3. Content-Language

· The XMLHttpRequest contains upload events (section 4.5 shows an example using upload events)

The CORS spec collectively refers to these HTTP methods as simple methods, and the HTTP headers as simple headers. If these rules seem like a bit of a hodgepodge, it’s because they are. There isn’t any rhyme or reason as to why those rules are defined that way. It’s a result of how the web has evolved over the past 20 years. But how these rules were defined isn’t as important as identifying and responding to preflight requests. The next section modifies the sample app to respond to the preflight for the DELETE request.

4.3. Identifying a preflight request

In the context of the CORS lifecycle diagram, the preflight request is the first request sent from the browser to the server, as shown in figure 4.8. If a preflight request is just another HTTP request, how do you distinguish it from actual requests? The first thing you need to do is figure out what a preflight request looks like.

Figure 4.8. Preflight request

There are three characteristics of a preflight request, as shown in figure 4.9 it uses the HTTP OPTIONS method , it has an Origin request header , and it has an Access-Control-Request-Method header .

Figure 4.9. The pieces of a preflight request

Figure 4.9 shows the preflight request from the sample with each of these three characteristics highlighted. All three characteristics must exist on the request for it to be a preflight request. If any one of these pieces is missing, the request isn’t a preflight. Let’s dig deeper into what each of these pieces is.

4.3.1. Origin header

In chapter 3 you learned that every CORS request must have an Origin header. The preflight is no different. Without the Origin header, the request isn’t a CORS request, and therefore it can’t be a preflight request.

The Origin header tells you where the request is coming from. The value of the Origin header on a preflight request is the same value as the Origin header on the actual request. So if you’re making a CORS request from http://localhost:1111, the Origin header on the preflight request will also have the value http://localhost:1111.

4.3.2. HTTP OPTIONS method

A preflight request must be made via the HTTP OPTIONS method, which is defined by the HTTP spec and isn’t specific to CORS. The HTTP spec (RFC2616) defines an OPTIONS request as “a request for information about the communication options available on the request/response chain.” This means that even before CORS, clients could use the OPTIONS method to learn more about an endpoint. When used outside of CORS, the OPTIONS method traditionally conveys which HTTP methods are supported on a particular URL. Table 4.1 shows an example of a non-CORS OPTIONS request to a server. The Allow response header is used to indicate which HTTP methods are supported at the /api/posts endpoint, without triggering an actual request to the /api/posts endpoint.

Table 4.1. What an HTTP OPTIONS request and response might look like in a pre-CORS world

HTTP request

HTTP response

OPTIONS /api/posts HTTP/1.1 User-Agent: Chrome Host: 127.0.0.1:9999 Accept: */*

HTTP/1.1 200 OK Allow: GET, POST

Note

You may have noticed that the preflight request itself is a cross-origin request. This was deemed acceptable by the CORS spec authors, because the preflight request is used in the way OPTIONS requests are intended.

An OPTIONS request with an Origin header is not necessarily a preflight request. To distinguish a regular OPTIONS request from a preflight OPTIONS request, a preflight request will always contain an Access-Control-Request-Method header, as discussed next.

4.3.3. Access-Control-Request-Method header

The Access-Control-Request-Method request header asks the server for permission to make a request using a particular HTTP method. The preceding example of deleting a post uses the HTTP DELETE method. Therefore, the Access-Control-Request-Method header would be set to DELETE:

Access-Control-Request-Method: DELETE

The Access-Control-Request-Method header is always set to the value of the HTTP method for the actual request, as shown in Table 4.2. Because an HTTP request must have an HTTP method, a preflight request must have an Access-Control-Request-Method header.

Table 4.2. Mapping the actual request method to the preflight

Preflight request

Actual request

OPTIONS /data HTTP/1.1 User-Agent: Chrome Host: localhost:10009 Accept: */* Origin: http://localhost:10007 Access-Control-Request-Method: DELETE

DELETE /data HTTP/1.1 User-Agent: Chrome Host: localhost:10009 Accept: */* Origin: http://localhost:10007

To recap, a preflight request must have an HTTP OPTIONS method, and it must contain an Origin and Access-Control-Request-Method header. Now that you know what comprises a preflight request, let’s modify the sample code to detect these three characteristics.

4.3.4. Putting it all together

Listing 4.4 adds an isPreflight method to the sample app that detects whether an incoming request is a preflight request. The isPreflight method checks three things:

· Is the request an HTTP OPTIONS request?

· Does the request have an Origin header?

· Does the request have an Access-Control-Request-Method header?

Listing 4.4. Checking if the request is a preflight

If all three criteria are true, the request is a preflight. If you reload the sample and click a Delete link, you should see the text “Received a preflight request!” in the server’s terminal window, as shown in figure 4.10.

Figure 4.10. The server successfully received a preflight request.

This verifies that the server has detected a preflight request. But the request still fails because you aren’t responding to the preflight. Next you’ll learn how to respond to the preflight.

4.4. Responding to a preflight request

Now that you have detected the preflight request, the server needs to respond so that the browser can make the actual request. The server’s preflight response, shown in figure 4.11, grants permissions to make the HTTP request. These permissions are granted by setting HTTP headers on the response. This section will show which HTTP headers the server needs to set to respond to a preflight, and how to reject a preflight if the request isn’t allowed.

Figure 4.11. After receiving a preflight request, the server sends a preflight response that may grant permissions to make the actual HTTP request.

4.4.1. Supporting HTTP methods with Access-Control-Allow-Methods

Thinking back to the conversation between the browser and the server in figure 4.1, responding to a preflight involves telling the browser that the server accepts DELETE requests from different origins. The server does this by setting the Access-Control-Allow-Methods response header as follows:

Access-Control-Allow-Methods: DELETE

This header indicates that the server grants permissions to the client to make a DELETE request to that URL. The Access-Control-Allow-Methods header may look a lot like the Access-Control-Request-Method header, but they’re quite different. The Access-Control-Request-Method is a single value that asks permission to use a specific HTTP method. The Access-Control-Allow-Methods header grants permissions to use one or more HTTP methods, and it can have multiple values. If you wanted to open your API endpoint to all HTTP methods, you can put the following values in the Access-Control-Allow-Methods header:

Access-Control-Allow-Methods: HEAD, GET, POST, PUT, DELETE

Let’s turn back to the sample code and update it to respond to the preflight request.

Updating the sample to support DELETE

The following listing modifies the sample code to respond to the preflight request. If the request is a preflight, the code adds an Access-Control-Allow-Methods header to the response.

Listing 4.5. Modifying handleCors to respond to a preflight

Now, finally, if you restart the server, reload the page, and click a Delete link, the blog post will be deleted. The deleted post will disappear from the page as a confirmation that the delete was successful. figure 4.12 shows the two HTTP requests being made to delete the post.

Figure 4.12. The preflight followed by the DELETE request

Note

If this were a real app, the corresponding blog post would be permanently deleted from the database. Because the sample doesn’t have a database, the deleted posts will reappear when you restart the web server.

In addition to the Access-Control-Allow-Methods header, the preflight response should have the following characteristics:

· The HTTP response status should be in the 200 range. This is defined by the CORS spec (although some browsers still process the response correctly if the status isn’t in the 200 range). The sample code uses response code 204, which indicates the response is valid, but contains no body.

· The response shouldn’t have a body. There isn’t anything in the CORS spec regarding the body of the preflight response, but having a body could cause confusion for the developer, because he or she then must know how to parse and interpret the body. It’s better to keep things simple and stick to the CORS headers only.

· If a method is a simple method, it doesn’t need to be listed in the Access-Control-Allow-Methods header. (Recall from section 4.2 that the CORS spec defines simple methods as GET, POST, and HEAD.) If the client sends anAccess-Control-Request-Method: GET request header, the server doesn’t need to include the Access-Control-Allow-Methods header in the response. But this can be confusing. For consistency, the rest of the samples in this chapter will always include the Access-Control-Allow-Methods header, even for simple HTTP methods.

With the changes done so far, the server can now support CORS for various HTTP methods. But we aren’t done with the preflight quite yet. A preflight request is also sent when the client adds additional headers to a request. The next section covers how to respond to those requests.

4.4.2. Supporting request headers with Access-Control-Allow-Headers

The previous section taught you how the server can respond to a preflight request to grant permissions to use HTTP methods. But this isn’t the only type of preflight a server can receive. A browser may also send a preflight if the request contains additional HTTP headers from the client. The modified browser/server conversation is shown in figure 4.13.

Figure 4.13. Conversation between browser and server for additional headers

Triggering a preflight for request headers

To see this in action, let’s modify the getBlogPost method to include a couple of additional headers when loading the blog posts. It really doesn’t matter what the headers are, so let’s make some up:

· Timezone-Offset— The user’s time zone offset in minutes. This can be calculated from JavaScript.

· Sample-Source— The name of this book, CORS in Action.

The following listing adds these headers to the sample code. Again, the actual values of these headers aren’t important; they are needed only to see how CORS behaves.

Listing 4.6. Adding headers to the request

var getBlogPosts = function() {

var xhr = createXhr('GET', 'http://127.0.0.1:9999/api/posts');

xhr.setRequestHeader('Timezone-Offset',

new Date().getTimezoneOffset());

xhr.setRequestHeader('Sample-Source',

'CORS in Action');

xhr.onload = function() {

...

When you reload the client page, none of the posts will load, and you’ll see an error message similar to the one you saw with the DELETE method, as shown in figure 4.14. Only this time the error is about the new headers and not the HTTP method. If you inspect the request in the Network tab as shown in figure 4.15, you can see that the preflight request has a new Access-Control-Request-Headers header with the additional header values.

Figure 4.14. Error when trying to add additional headers to a request

Figure 4.15. Preflight request has a new Access-Control-Request-Headers header

Note

It may be surprising to see that the preflight request is sent even though the request is a GET request. After all, didn’t we add support for GET requests in chapter 3? Adding custom headers to a cross-origin request is a new functionality that wasn’t possible before CORS. Therefore, it needs a preflight, even if the HTTP method wouldn’t normally trigger a preflight.

In the same way that the Access-Control-Request-Method header asks permission to use a particular HTTP method, the Access-Control-Request-Headers header asks permission to send additional headers to the server. Table 4.3shows how the request maps onto the Access-Control-Request-Headers header.

Table 4.3. Mapping the actual request headers to the preflight

Preflight request

Actual request

OPTIONS /api/posts HTTP/1.1 User-Agent: Chrome Host: 127.0.0.1:9999 Accept: */* Origin: http://localhost:1111 Access-Control-Request-Method: GETAccess-Control-Request-Headers: Timezone-Offset, Sample-Source

GET /api/posts HTTP/1.1 User-Agent: Chrome Host: 127.0.0.1:9999 Accept: */* Origin: http://localhost:1111 Timezone-Offset: 300Sample-Source: Cors in Action

The Access-Control-Request-Headers header serves a similar purpose as its Access-Control-Request-Method counterpart, but there are differences. While the Access-Control-Request-Method header can have only one value, the Access-Control-Request-Headers header can have multiple values separated by a comma. And while a preflight request will always have an Access-Control-Request-Method header, the Access-Control-Request-Headers header is optional, and is only present if the client adds headers to the request.

Allowing custom headers on the request

The server responds to the preflight request by adding the Access-Control-Allow-Headers header. The Access-Control-Allow-Headers header contains a list of headers that are allowed in requests. The following response header indicates that the client has permission to include the Timezone-Offset and the Sample-Source headers on requests:

Access-Control-Allow-Headers: Timezone-Offset, Sample-Source

If all the values in the Access-Control-Request-Headers request header match the values in the Access-Control-Allow-Headers response header, the browser is granted permission to make the request. If the browser requested a header, and that header isn’t present in the Access-Control-Allow-Headers header, the request is rejected. Table 4.4 shows an example of a valid and an invalid Access-Control-Allow-Headers header.

Table 4.4. Responding to Access-Control-Request-Headers by using Access-Control-Allow-Headers. All requested headers must also be in the response for the CORS request to succeed.

Response header

Preflight request status

Access-Control-Allow-Headers: Timezone-Offset, Sample-Source

Accepted. Timezone-Offset and Sample-Source were requested, and both are present in the Access-Control-Allow-Headers header.

Access-Control-Allow-Headers: Timezone-Offset, Sample-Source, Anything-Else

Accepted. Same as the previous case. Even though the Anything-Else header is not present in the request, it’s okay to specify additional values in the Access-Control-Allow-Headers header.

Access-Control-Allow-Headers: Timezone-Offset

Rejected. Both Timezone-Offset and Sample-Source were requested, but only Timezone-Offset is present in the Access-Control-Allow-Headers header.

Listing 4.7 recaps how the browser translates additional headers from code into the preflight request, and how these headers flow from preflight request to preflight response to actual request. The preflight starts in the developer’s JavaScript code, which adds new headers to the XMLHttpRequest . The browser notices that there are additional headers and puts them in the Access-Control-Request-Headers header in the preflight . The server responds by including those same headers in the Access-Control-Allow-Headers response . Finally, the browser validates that the headers match those requested by the developer, and sends the request to the server .

Listing 4.7. Flow of additional headers on CORS requests

Note

If the requested header is a simple header, it’s not required to be included in the Access-Control-Allow-Headers response header. But I recommend including simple headers to avoid confusion.

Listing 4.8 modifies the sample code to respond to the Access-Control-Request-Headers header. The code sets the Access-Control-Allow-Headers header to the value of the headers you support. It also adds the GET method to the list of allowed HTTP methods, because the custom headers are sent on a GET request. The sample will work fine even without adding GET to the Access-Control-Allow-Methods header (because GET is a simple method), but I like to include it to avoid confusion. Restart the server and reload the page, and the blog posts should reappear.

Listing 4.8. Responding to Access-Control-Request-Headers

After the server responds to the preflight request, the browser inspects the preflight response and verifies that the server is granting the appropriate permissions. If the preflight response checks out, the browser sends the actual request to the server.

4.4.3. Sending the actual request

Once the browser receives a successful preflight response, it sends the actual request to the server, as shown in figure 4.16.

Figure 4.16. Sending the actual request

In the blogging app, once the browser receives the preflight response, it sends the HTTP DELETE request to delete the blog post. The DELETE request can be handled using the same technique you learned in chapter 3: add an Access-Control-Allow-Origin header.

The following listing highlights the code that adds the Access-Control-Allow-Origin header to the response. This code is from chapter 3. You don’t need to write any new code—the Access-Control-Allow-Origin header is added to both preflight and actual responses.

Listing 4.9. Adding the Access-Control-Allow-Origin header to all CORS responses

We’ve spent a lot of time talking about how to successfully respond to a preflight request. But there may be times when you don’t want a request to be made. Next, let’s turn our attention to rejecting a preflight request.

4.4.4. Rejecting a preflight request

We’ve explored how to successfully respond to a preflight request. But there may also be times when you want to reject a CORS request. Perhaps your server doesn’t support the DELETE method at a particular endpoint. How do you tell the browser that the request isn’t allowed?

Rejecting a CORS request “short-circuits” the request, as shown in figure 4.17. The browser makes the preflight request to the server, and when the server rejects the request, the browser notifies the client code that the request was rejected. The client code doesn’t receive the actual preflight response, nor does it receive any additional data about why the request failed (even though the console log shows this information).

Figure 4.17. If the server rejects a preflight request, the browser returns an error to the client without ever sending the actual request.

As mentioned in chapter 1, servers must opt-in to CORS. That means if a server’s response doesn’t exactly match what the browser expects, the browser plays it safe and rejects the request. With that in mind, there are many ways for a server to reject a preflight request, including:

· Leave out the Access-Control-Allow-Origin header (if the requested method is not a simple method).

· Return a value in Access-Control-Allow-Methods that doesn’t match the Access-Control-Request-Method header.

· If the preflight request has an Access-Control-Request-Headers header:

o Leave out the Access-Control-Allow-Headers header.

o Return a value in the Access-Control-Allow-Headers header that doesn’t match the Access-Control-Request-Headers header.

Returning a non-200 HTTP response code as the preflight response will not reject the request in some browsers. This may sound surprising, because a non-200 status code is used to signal that something isn’t right. But in this case, even though the CORS spec explicitly states that the preflight response should be in the 200 range, some browsers still allow non-200 responses. It’s still a good idea to stick to the HTTP 200 or 204 status code, because it adheres to the spec, which won’t change.

Table 4.5 shows ways to reject the preflight request. Suppose someone tries to send a request header named Shady-Status to the sample app. You don’t want your server to receive this header, so the server code should reject the preflight.

Table 4.5. Various ways to reject a CORS preflight request

Response

Reason

HTTP/1.1 200 OK

No Access-Control-Allow-Origin header

HTTP/1.1 200 OK Access-Control-Allow-Origin: *

No Access-Control-Allow-Methods header

HTTP/1.1 200 OK Access-Control-Allow-Origin: * Access-Control-Allow-Methods: DELETE

No Access-Control-Allow-Headers header

HTTP/1.1 200 OK Access-Control-Allow-Origin: * Access-Control-Allow-Methods: DELETE Access-Control-Allow-Headers: Foo

Access-Control-Allow-Headers doesn’t match

HTTP/1.1 200 OK Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET Access-Control-Allow-Headers: Shady-Status

Access-Control-Allow-Methods doesn’t match

OPTIONS /api/posts HTTP/1.1

Origin: http://localhost:1111

Access-Control-Request-Method: DELETE

Access-Control-Request-Headers: Shady-Status

As you can see, there are a lot of ways to reject a preflight request. Which one should you choose? Chapter 6 provides guidance for rejecting CORS requests.

The last few sections threw a lot of information your way. The next section takes a step back and reviews what you’ve learned so far.

4.5. Recapping preflights

The first thing you may have noticed is that there are a lot of different headers involved when working with preflights. Table 4.6 recaps these headers and what they mean.

Table 4.6. Preflight request headers and their corresponding response headers

Request header

Details

Response header

Details

Access-Control-Request-Method

Indicates the HTTP method for the actual request

Access-Control-Allow-Methods

Indicates which HTTP methods are supported at that endpoint Can contain multiple values

Access-Control-Request-Headers

Indicates additional headers on the request Can contain multiple values Optional

Access-Control-Allow-Headers

Indicates which HTTP headers are supported at that endpoint Can contain multiple values

A simple way to distinguish these headers is to remember that any header starting with “Access-Control-Request-” is a request header added by the browser asking the server for permissions, and any header starting with “Access-Control-Allow-” is a response header sent by the server that grants permissions.

Figure 4.18 revisits the end-to-end flow from figure 4.2, except this time it includes the header and code that is exchanged among the client, browser, and server. The JavaScript code initiates the request by calling theXMLHttpRequest’s send method . The browser intercepts the request and initiates a preflight request . If the server provides a valid preflight response , the browser follows up by sending the actual request . The server responds to the actual request . This actual request is then sent to the calling JavaScript code for further processing .

Figure 4.18. End-to-end CORS request flow (with preflight)

Let’s wrap up our discussion on preflights by looking at a few things to keep in mind about preflight requests.

Successful preflight != successful request

The preflight response doesn’t provide any insight into the success or failure of the actual request. A preflight could be successful, but the request could still fail for many reasons, such as a file not found, an authorization error, or a server issue. The preflight only ensures that the browser can make a cross-origin request to the server, and nothing more. The server is still free to reject the request for other reasons.

Think of an HTTP request as a set of Russian nesting dolls. Each doll contains a set of request headers that define the request behavior. Figure 4.19 shows how an HTTP request maps to this Russian nesting doll analogy.

Figure 4.19. An HTTP request as a set of Russian dolls. Requests may fail for various reasons, and a successful preflight doesn’t imply a successful request.

The outermost doll is the CORS doll, and it contains the Origin header. The inner dolls can contain a variety of information, such as a cookie validating the user (cookie support in CORS will be covered in the next chapter). Even if the outermost CORS layer succeeds, the request may still fail while processing through one of the inner layers (for example, the cookie may have expired).

JavaScript code and preflights

The preflight takes place solely between the browser and the server. There is no way for the JavaScript code to intercept the preflight response or get updates on its status. From the client code’s perspective, the preflight is invisible. A failing preflight is akin to the actual request failing, even though the actual request is never made.

This is a useful feature, because it hides the complexity of preflight requests from the developer writing the client code. Figure 4.20 shows how both the client and the server view a CORS request with a preflight. There is complexity in answering a preflight request, but only the server developer needs to worry about this complexity. From the client developer’s perspective, a cross-origin request looks the same as any HTTP request.

Figure 4.20. A CORS request (with preflight) looks like a regular HTTP request from the client’s perspective.

Preflights are stateless

Both the preflight and the actual request are stateless. This means that there is no additional information connecting the actual request back to the preflight request that preceded it. To get a sense of what this means, table 4.7compares a preflight request with its partner request.

Table 4.7. The actual request has no information about the preflight request

Request type

HTTP request

Preflight

OPTIONS /api/posts HTTP/1.1 User-Agent: Chrome Host: 127.0.0.1:9999 Accept: */* Origin: http://localhost:1111 Access-Control-Request-Method: GET Access-Control-Request-Headers: Timezone-Offset, Sample-Source

Actual

GET /api/posts HTTP/1.1 User-Agent: Chrome Host: 127.0.0.1:9999 Accept: */* Origin: http://localhost:1111 Timezone-Offset: 300 Sample-Source: Cors in Action

Looking at the actual request, there is no information that connects it to the original preflight request. This can be confusing because we think of the preflight plus the actual request as occurring in tandem. If your server receives the actual request, you have to trust that the browser did the right thing and sent the preflight request before it. This highlights how crucial the browser is when making a CORS request.

Because the preflight request is stateless, it’s important that all CORS responses include the Access-Control-Allow-Origin header. It’s not enough to include the Access-Control-Allow-Origin header on just the preflight response. Both the preflight response and the actual response need the Access-Control-Allow-Origin header, as shown in table 4.8.

Table 4.8. Both the preflight response and the actual response need the Access-Control-Allow-Origin header.

Response type

HTTP response

Preflight

HTTP/1.1 204 No Content Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, DELETE Access-Control-Allow-Headers: Timezone-Offset, Sample-Source

Actual

HTTP/1.1 200 OK Access-Control-Allow-Origin: * Content-Type: application/json

If neither the preflight response nor the actual response has an Access-Control-Allow-Origin header, the CORS request will be rejected.

Preflight requests and upload events

The discussion in this chapter has been focused on making HTTP requests, but a preflight request can also be issued on upload events. Upload events provide progress information about an upload. The following code shows how to use upload events to show a status message for an upload:

function uploadFile(file) {

var xhr = new XMLHttpRequest();

xhr.open('POST', '/upload', true);

xhr.upload.onprogress = function(e) {

console.log('Upload progress: ' ((e.loaded / e.total) * 100));

};

xhr.send(file);

}

The request needs to have a preflight because upload events are a new concept. Before upload support in XMLHttpRequests existed, the traditional way of doing an upload was through a form. While the form would upload a file, it couldn’t provide additional information, such as how far the upload has progressed. The upload event introduces a functionality that wasn’t available before CORS; therefore it requires a preflight request.

Upload events are a purely client-side feature: the browser fires the event, and doesn’t need to make a server request for more information. But the upload request still requires a preflight because it’s introducing functionality that didn’t exist before CORS. On the other hand, if you upload a file using XMLHttpRequest, but without using upload events, the request doesn’t need a preflight (again, this makes sense, because without upload events, the upload behaves the same as a form upload). Table 4.9 shows the distinction between the two types of uploads.

Table 4.9. Uploading with and without upload events. Uploading without upload events doesn’t need a preflight.

JavaScript code

Preflight status

var xhr = new XMLHttpRequest();

xhr.open('POST', '/upload', true);

xhr.upload.onprogress = function(e) {

console.log('Upload progress: ' ((e.loaded / e.total) * 100));

};

xhr.send(file);

Uses upload events; requires a preflight

var xhr = new XMLHttpRequest();xhr.open('POST', '/upload', true);

xhr.send(file);

Doesn’t use upload events; no preflight necessary

While this section has done its best to catalog all the cases where a preflight request is made, there may be new types of requests in the future that issue a preflight. But as long as your server is armed with the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers, you’ll be prepared to answer any preflight request that comes your way.

4.6. Preflight result cache

One downside of the preflight request is that it issues two HTTP requests, one for the preflight and a second for the actual request. This can be a performance concern because HTTP requests are expensive, especially on resource-constrained devices like smartphones. To help reduce the number of preflight requests, preflight responses can be cached in a preflight result cache.

The preflight result cache stores the responses to a preflight request for a particular URL. If a request is made to the same URL, the browser first checks the preflight result cache to see if there is already a response. If the browser finds a response in the cache, it skips the preflight request and goes straight to the actual request. If there is no response in the cache, the browser sends the preflight, and then stores the response of that preflight in the cache. Figure 4.21 shows how responses flow through the preflight result cache.

Figure 4.21. Using the preflight cache. The first request sends a preflight to the server, but the second request grabs the preflight response from the preflight cache.

You can see the preflight cache in action by reloading the page a few times. The first time you reload the page, you’ll see the preflight OPTIONS request in Chrome’s Network tab. But subsequent reloads will skip the preflight request and go straight to the actual request. Figure 4.22 shows three GET requests fired one after the other, with only one preflight request. The preflight is sent for the first request only; the other two requests take advantage of the preflight result cache.

Figure 4.22. Multiple GET requests to the same URL using the preflight result cache

Note

To isolate just the CORS requests in figure 4.22, I selected the Preserve Log Upon Navigation option, which is the fifth icon from the left along the bottom of the console log. I also selected only to show XHR requests, so the requests to client.html are filtered out.

The cache stores entries by URL plus origin. Requests from different origins to the same URL all have different cache entries. Likewise, requests from the same origin to different URLs all have different cache entries. Because the preflight result cache can only store entries based on origin and URL, you can’t take shortcuts and specify a preflight response for an entire domain. Each URL in your domain that supports CORS will need to be able to respond to a preflight request.

While you can examine network traffic to see whether a preflight request was sent, there is no way to view the contents of the preflight request cache. The cache is a black box that is maintained internally by the browser. But you can have some control over how long items stay in the cache by using the Access-Control-Max-Age header.

Setting the cache time with Access-Control-Max-Age

CORS gives servers some control over how long a preflight response is cached through the Access-Control-Max-Age response header. The Access-Control-Max-Age header indicates how long, in seconds, a response can stay in the cache.

The following listing modifies the sample app to cache preflight requests for two minutes. This means that after the first preflight response is cached, the browser will check the cache before every request to the same URL for the next two minutes. After those two minutes are up, the browser will send a preflight request again.

Listing 4.10. Adding the Access-Control-Max-Age response header

The Access-Control-Max-Age value is only a suggestion for how long an item should be cached. Browsers may cache for a shorter amount of time. Firefox doesn’t allow items to be cached for longer than 24 hours, while Chrome, Opera, and Safari cache items for a maximum of five minutes. If the Access-Control-Max-Age header isn’t specified, Firefox doesn’t cache the preflight, while Chrome, Opera, and Safari cache the preflight for five seconds.

Note

The preceding browser-specific numbers were pulled from looking at the browser’s source code, and this behavior may change in the future. It’s not clear how Internet Explorer’s preflight cache behaves, because it isn’t documented and Internet Explorer’s code is not open source.

4.7. Summary

This chapter introduced the concept of a CORS preflight request:

· The browser sends a preflight request to ask the server for permission to make the actual request.

· The preflight request protects servers from receiving unexpected requests.

· The preflight request asks permissions to make requests with certain HTTP methods and/or add custom HTTP headers to the request.

· The preflight request takes the form of an HTTP OPTIONS method with an Origin and Access-Control-Request-Method header.

· The server can grant permissions to use certain HTTP methods by using the Access-Control-Allow-Methods header. The server can also grant permission to use certain HTTP headers by using the Access-Control-Allow-Headers header.

· The preflight result cache is a performance optimization that helps reduce the number of preflight requests made to a particular endpoint.

The previous chapter and this one introduced you to the headers to handle most types of CORS requests. But there are still some features, such as cookie support, that are not covered by the headers in this chapter. Chapter 5 takes a look at how to support these remaining features on CORS requests.