Demystifying Cross-Origin Resource Sharing (CORS) on Web
October 06, 2023• ☕️ 4 min read
Imagine you’re working on the front-end of a web application, integrating a REST API endpoint provided by your back-end colleague. You swiftly craft an AJAX call using a few lines of JavaScript with the fetch API. However, to your surprise, your browser console lights up with perplexing CORS errors. As you investigate further in the browser’s network tab, you encounter an additional HTTP request with the OPTIONS
method, an unfamiliar sight that adds to your frustration. What was expected to be a quick 5-minute task has led you down a rabbit hole, unraveling the complexities of the CORS protocol.
In the past, I faced a similar challenge and resorted to a temporary fix by installing a CORS extension in Chrome. Looking back, I wish I had taken the time to fully comprehend this concept.
This article aims to guide you through the intricacies of CORS, providing clarity and demystifying its purpose. Let’s dive in and unravel the mystery of CORS.
What is Cross-Origin Resource Sharing (CORS)
As per MDN documentation, the Cross-Origin Resource Sharing (CORS) is a HTTP-header based mechanism by which a server indicates to the browser of any origins other than its own origin from which it should permit loading resources.
To understand this definition, we need to understand the meaning of origin. An origin is a combination of scheme, host, and port. For a given URL, https://store.company.com:443
, the breakdown would be:
- Scheme: https
- Host: store.company.com
- Port: 443
For two origins to be the same, these three values should match. The following table gives examples of origin comparisons with the URL http://store.company.com/dir/page.html:
URL | Origin Type | Reason |
---|---|---|
http://store.company.com/dir2/other.html | Same Origin | Only the path differs |
http://store.company.com/dir/inner/another.html | Same Origin | Only the path differs |
https://store.company.com/page.html | Cross-Origin | Different protocol |
http://store.company.com:81/dir/page.html | Cross-Origin | Different port (http:// is port 80 by default) |
http://news.company.com/dir/page.html | Cross- Origin | Different host |
With this new knowledge of Origin
, let’s look at a few practical examples and understand what requests use CORS.
When you load a URL https://domain-a.com in your browser, the browser will parse the initial web document and make requests for other resources like JS, CSS, and images. All these requests to https://domain-a.com/*
are known as Same-origin requests and are always allowed by your browser. But if any resources were coming from another origin, for example, https://domain-b.com
, these requests are known as Cross-origin requests and are controlled by Cross-Origin Resource Sharing (CORS) mechanism.
Cross-origin requests are controlled by the CORS protocol
The following diagram will make it more clear. Note that the main request to the initial document (e.g., index.html) defines the origin.
Another example of a CORS request would be if JavaScript code (browser script) served from https://domain-a.com
initiates a Fetch API request for https://domain-b.com/api/products
.
For security reasons, browser restricts cross-origin HTTP requests initiated from scripts—the front-end JS code running in your browser. It means if you are making a REST API call using FETCH or XMLHttpRequest to a different origin, you might get some CORS errors in your console unless the response from the other origin server includes the right CORS headers. This is due to the fact that both FETCH and XMLHttpRequest follow the same-origin-policy security mechanism.
These requirements of user-agents (browsers) are part of fetch algorithm. From a server developer perspective, you need to specify a set of headers indicating whether a response can be shared cross-origin or not.
Let’s dig a little deeper into this CORS protocol and understand what steps the browser takes to mitigate risk associated with unsafe cross-origin requests.
Simple and Preflight requests
While accessing a resource on another origin, a request is considered Simple when it fulfills all of the following requirements:
- has one of the allowed methods:
- GET, HEAD, POST. These methods are also called CORS-safelisted methods in Fetch specification.
- has one of the CORS-safelisted request-headers
- has one of the media-type specified in the
Content-Type
headers:application/x-www-form-urlencoded
,multipart/form-data
,text/plain
- No event listener is attached to
xhr.upload.addEventListener
when making a request usingXMLHttpRequest
(xhr being an instance of XMLHttpRequest) - No ReadableStream object is used in the request.
This list of requirements has been taken straight from MDN Simple Request documentation.
Whenever a request doesn’t meet the criteria of a simple request, it’s considered a CORS-preflight request. As part of user-agent requirements mentioned in the fetch algorithm, the browser will automatically craft these preflight requests for your front-end JS code. Front-end Developers don’t need to make these requests explicitly.
For example, if your front-end JavaScript code makes a fetch request to another origin, the following steps occur:
- Based on the request parameters that your JS code is trying to make, if it’s not a simple request, the browser will mark it as a
to be preflight
request. - The browser will then automatically generate an
OPTION
request and can include the following headers:- Access-Control-Request-Method: indicates to the server which HTTP method the future CORS request to the same resource might use.
- Access-Control-Request-Headers: indicates which HTTP headers a future CORS request to the same resource might use.
- The server, upon receiving this
CORS-preflight
request, can respond with the following headers:- Access-Control-Allow-Methods: indicates the list of permitted HTTP methods to be used with actual request.
- Access-Control-Allow-Headers: indicates the list of permitted HTTP headers to be used with actual request.
- Access-Control-Allow-Origin: indicates the
origin
that is allowed to access this requested resource. The value can be a*
(wild card), which means that the resource can be accessed by any origin. - Access-Control-Max-Age: indicated the number of seconds (5 by default) the response of the preflight request can be cached.
To understand it better let’s consider an example of a script running in your browser making a Fetch API call. This fetch request is being generated from origin https://domain-a.com
to another origin https://domain-b.com
.
fetch("https://domain-b.com/api/todos", {
method: "POST",
body: JSON.stringify({
name: "learn about CORS",
isCompleted: false,
}),
headers: {
"Content-Type": "application/json",
"X-Aman-Explains": "awesome",
},
});
This example script is sending an XML body with a POST
request. Since the request uses Content-Type
of application/json
along with a non-standard HTTP X-Aman-Explains
request header, it no longer meets the criteria of a Simple
request. The browser will initiate a preflight request with OPTION
method, including two additional headers as discussed above.
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-Aman-Explains, Content-Type
Upon receiving this preflight request, the server will respond with the following headers:
Access-Control-Allow-Origin: https://domain-a.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-Aman-Explains, Content-Type
Access-Control-Max-Age: 3600
By sending Access-Control-Allow-Origin: https://domain-a.com
, the server is stating that this origin is allowed to access this resource in the actual request. Additionally, it indicates that POST
, and GET
are valid methods to query the resource without any concerns. Moreover, as shown in the comma-separated list of allowed headers, it confirms that X-Aman-Explains
and Content-Type
are valid HTTP headers and are allowed to be used in the actual request as well.
Lastly, the server is happy for the browser to cache this preflight response for 1 hour (3600 seconds), and thus no need to make another preflight request within this period.
Note: Each browser has a maximum internal value that takes precedence when the Access-Control-Max-Age exceeds it.
Once the browser receives this preflight response, it can introspect the CORS HTTP response headers and check that these custom headers (Content-type of application/json
and X-Aman-Explains
) are allowed by the https://domain-b.com
origin. Based on this information, the browser then sends an actual request that results in a JSON response from the server.
Summary
CORS is an HTTP-header based mechanism that allows the origin that initially loads your main document to access resources from another origin.
When executing a request that is not a simple request, the browser sends out a preflight request with OPTION
method, along with additional headers, asking the other origin if it’s safe to make the actual future request or not. If it’s approved, the browser will go ahead and fire the actual request. Otherwise, the request will result in CORS failure and an error will be displayed in your browser console.
Resources
- HTTP CORS protocol - WHATWG Fetch Spec
- CORS - MDN Documentation
- Understanding ‘same-site and ‘same-origin’
- CORS - OWASP Cheatsheet
Written by Amandeep Singh. Developer @ Avarni Sydney. Tech enthusiast and a pragmatic programmer.