Skipping preflight checks
Preflight checks are part of the CORS system that browsers fire before cross-origin requests in order to determine if the request is allowed. These requests can add up over time and cause a discernable lag in your web application.
If the architecture of your application involves a frontend JavaScript SPA served from one domain and a backend API server served on another, then CORS requests are your reality. For example, our frontend is served from AWS CloudFront on https://platform.culturehq.com
, while our API is served from https://api.culturehq.com
.
However, if these resources are served on the same parent domain (in this case culturehq.com
), there is a way to skip preflight checks entirely. Below is a discussion of how that can happen, as well as a bit of background on the topics mentioned so far.
CORS
CORS is a system built with HTTP headers that is used to determine whether or not JavaScript code is allowed to access resources on certain servers. By default, due to the same-origin security policy, JavaScript code cannot request resources on servers that do not match the current origin (as determined by document.domain
). However, by configuring CORS headers you can open up certain resources to certain HTTP methods and headers, thereby allowing you server to be accessed from various cross-origin JavaScript resources.
document.domain
When browsers are determining whether or not to issue a preflight check, they’ll check the document.domain
property. If the value matches up with the domain of the requested resource, a preflight check doesn’t get issued. You can change this value at any time from within JavaScript code to be either the current domain or any superdomain of the current domain (for instance, on the platform.culturehq.com
domain you can set it to culturehq.com
). Then, if you’re requesting resources on the other domain preflight checks will be skipped. The problem still exists however if you’re trying to access a separate subdomain (as in api.culturehq.com
). In this case you’ll need to change the domain on both the server and the client.
Aligning the API
In order to change the domain of the server such that preflight checks will be skipped entirely across subdomains, we can get clever. The first step is to configure an endpoint on the server that will return a very simple HTML response that includes the JavaScript to set the domain to the superdomain. The example below configures a Ruby on Rails
application to serve up just that kind of page:
PROXY_RESPONSE = <<~HTML
<!DOCTYPE html>
<html>
<body>
<script>document.domain = 'culturehq.com'</script>
</body>
</html>
HTML
Rails.application.routes.draw do
get :proxy, to: lambda { |_env|
[200, { 'Content-Type' => 'text/html' }, [PROXY_RESPONSE]]
}
end
In the above example, we’ve configured the /proxy
endpoint to return an HTML document that will immediately set the document.domain
property to culturehq.com
. Then, any requests made from this page will pass the same-origin check if they’re also issued from the culturehq.com
domain.
Aligning the frontend
On the frontend, we can then embed an iframe
within the current page that requests that proxy page, and steal the fetch
function from it in order to issue requests to our API.
const fetcher = { fetch: window.fetch.bind(window) };
const skipPreflightChecks = () => {
const iframe = document.createElement("iframe");
iframe.onload = function () {
const { fetch } = this.contentWindow;
fetcher.fetch = fetch.bind(this.contentWindow);
};
iframe.setAttribute("src", "https://api.culturehq.com/proxy");
iframe.style.display = "none";
document.domain = "culturehq.com";
document.body.appendChild(iframe);
};
Using the above code we can then call skipPreflightChecks()
, and once it resolves we can use fetcher.fetch
to issue requests that pass the same origin check.
tl;dr
Preflight checks are triggered because of the same-origin check. You can change the domain of your webpage by modifying document.domain
. Given these facts, you can embed an iframe in your webpage that sets its own domain to your top-level domain as well as setting your domain to your top-level domain in your frontend to bypass these checks and achieve preflight-less cross subdomain requests that are faster to resolve.