Cross-Origin Resource Sharing (CORS) opens up some awesome possibilities for accessing APIs from JavaScript. But certain characteristics of REST APIs work against CORS:

  • JSON APIs return a Content-Type of application/json (while other formats have their own content types).
  • API resources are addressed with unique URLs.

Measure this against the rules for CORS preflights:

  • A request for a Content-Type other than application/x-www-form-urlencoded, multipart/form-data, or text/plain triggers a preflight request. This means that every request to a JSON API (even GET or POST requests) triggers a preflight.

  • In order to reduce the number of preflight requests, CORS has the concept of a preflight cache. However the preflight information is cached for an origin/url pair. That means each unique url has its own preflight cache. An API with the same preflight response across all urls will still receive a preflight request for each unique request.

  • Finally consider the fact that WebKit browsers cache preflight response for a maximum of 5 minutes (source code). According to a comment in the code, this is to prevent cache poisoning. (Firefox has a limit of 24 hours, I have no idea if IE has a limit). This cache limit can be misleading: even with an Access-Control-Max-Age value of, say, 3600 (seconds), developers will see a new preflight request after only 5 minutes.

Taken together, these issues limit the usefulness of the preflight cache. The most useful case would be an app that hits the same endpoint over and over again (maybe some sort of polling mechanism?). But for a long running app making requests to a variety of urls (a common scenario for most web apps), the cache does little to avoid the preflight performance hit.

There are some hacks that can help improve on this:

  • The API content type can return one of the accepted “simple” values for Content-Type, such as application/x-www-form-urlencoded, multipart/form-data, or text/plain. This will avoid the preflight request altogether (for GET/POST only, and only if there are no other custom headers)

  • Variables in the path can be moved to query parameters. This reduces the API’s url space, and fewer urls means a higher preflight cache hit ratio.

  • At the extreme, an API format that funnels all requests through a single url (such as JSON-RPC) will ensure that the preflight cache applies to all requests.

Of course, all these suggestions sacrifice the “RESTfulness” of the API (if you're into that sort of thing).

Ideally, it would great if preflight information could be cached for an entire domain or a wildcard path. I proposed this on the W3 publicapps mailing list, but there are concerns over how to do this securely. Still, I think finding some solution to this issue would improve performance and make CORS a more palatable option for APIs.