Two Strategies for Crossing Origins with Performance in Mind

The web's same-origin policy is one of the cornerstone mechanisms upon which web security is built. It restricts the way that resources can be shared between web applications that have differing protocols, hosts or ports.

CORS Performance

There are two types of CORS requests. Simple requests, which browsers and servers treat as they always have. "Complex" requests, on the other hand, require an additional "preflight check" to ensure that the server is aware of and prepared for requests from another origin. Preflight checks are accomplished using a separate HTTP OPTIONS request, along with several headers that describe to the server the client's origin, the HTTP method it would like to execute and the headers it will send. With the preflight description in hand, the server can choose to indicate if it would attempt to process a request like that or not. If the indication is positive, then the client issues the actual request.

The challenge with CORS comes from these preflight HTTP requests, which introduce additional round trips and reduce performance. That said, it's no surprise that eliminating as many preflight requests as possible is desired. After all, minimizing the number of HTTP requests has long stood as the number one best practice in web performance.

There are two strategies for reducing preflight requests:

1. Design your API and/or serve your content so that is only uses these HTTP methods and headers:

HEAD

GET

POST

Accept

Accept-Language

Content-Language

Content-Type (but only with a value of application/x-www-form-urlencoded, multipart/form-data or text/plain)

To retain backwards compatibility, cross-origin requests that meet this criteria do not require preflight requests. These restrictions are quite limiting, however, a little cleverness with the resource's URI can open up all sort of possibilities. For example, your application could provide semantics to override the HTTP method via the query string (e.g. http://example.com?method_override=DELETE). A similar technique could be used in place of headers.

For the readers like me, the impurity of this suggestion might make you a bit queasy. There certainly are scenarios where this technique fits the bill just fine, and others where it should be avoided. You'll have to weight the merits of purity versus performance for your own application.

2. Enable caching on preflight responses to reduce repeat checks.

Just like with standard HTTP requests, proper caching will improve CORS performance. Unfortunately, we can't use the familiar Cache-Control header to configure preflight response caching policies.

Instead, the CORS specification introduces a new header: Access-Control-Max-Age. Access-Control-Max-Age has a simple numeric value, indicating the number of seconds in which the browser should cache a preflight response. For best effect, set the value of Access-Control-Max-Age to the longest duration that you can stomach.

For security purposes, the Resource Timing specification restricts many useful attributes from being disclosed for CORS requests, including:

redirectStart

redirectEnd

domainLookupStart

domainLookupEnd

connectStart

connectEnd

requestStart

responseStart

secureConnectionStart

This restriction can be lifted by adding a Timing-Allow-Origin header to responses with a value which lists all the allowed origins (in a comma delimited list), or a value of '*' to allow all origins.

While Timing-Allow-Origin doesn't affect the performance of your service the way that reducing preflight requests does, it does allow consumers to get much finer grained detail and insight into the performance you do have, and I consider that a win.