Consider the following situation: you’re trying to fetch some data from an API on your website using fetch()
but end up with an error.
You open up the console and see either “No Access-Control-Allow-Origin
header is present on the requested resource,” or “The Access-Control-Allow-Origin
header has a value <some_url>
that is not equal to the supplied origin” written in red text, indicating that your request was blocked by CORS policy.
Seem familiar? With over 10,000 questions posted under the cors
tag on StackOverflow, it is one of the most common issues that plague frontend developers and backend developers alike. So, what exactly is the CORS policy and why do we face this error often?
What is Cross-Origin Resource Sharing (CORS)?
Interestingly, this is not an error as we portray it, but rather the expected behavior. Our web browsers enforce the same-origin policy, which restricts resource sharing across different origins. Cross-origin resource sharing, or CORS, is the mechanism through which we can overcome this barrier. To understand CORS, let us first understand the same-origin policy and its need.
The same-origin policy
In simple terms, the same-origin policy is the web version of “don’t talk to strangers” incorporated by the browser.
All modern web browsers available today follow the same-origin policy that restricts how XMLHttpRequest
and fetch
requests from one origin interact with a resource from another origin. What’s an origin, exactly?
It’s the combination of a scheme, domain, and port. The scheme could be HTTP, HTTPS, FTP, or anything else. Similarly, the port can also be any valid port number. Same-origin requests are essentially those requests whose scheme, domain, and port match. Let’s look at the following example.
Assuming our origin is http://localhost:3000
, the requests can be categorized into same-origin or cross-origin requests as follows:
Origin | Request Type | Reason |
---|---|---|
http://localhost:3000/about | Same-origin | The path “/about” is not considered as a part of the origin |
http://localhost:3000/shop/product.html | Same-origin | The path “/shop/product.html” is not considered as a part of the origin |
http://localhost:5000 | Cross-origin | Different port (5000 instead of 3000) |
https://localhost:3000 | Cross-origin | Different scheme (HTTPS instead of HTTP) |
https://blog.logrocket.com | Cross-origin | Different scheme, domain, and port |
This is the reason why your frontend running on http://localhost:3000
cannot make API calls to your server running http://localhost:5000
or any other port when you develop single-page applications (SPAs).
Also, requests from origin https://mywebsite.com
to origin https://api.mywebsite.com
are still considered cross-site requests even though the second origin is a subdomain.
Due to the same-origin policy, the browser will automatically prevent responses from cross-origin requests from being shared with the client. This is great for security reasons! But not all websites are malicious and there are multiple scenarios in which you might need to fetch data from different origins, especially in the modern age of microservice architecture where different applications are hosted on different origins.
This is a great segue for us to deep dive into CORS and learn how to use it in order to allow cross-origin requests.
Allowing cross-site requests with CORS
We’ve established that the browser doesn’t allow resource sharing between different origins, yet there are countless examples where we are able to do so. How? This is where CORS comes into the picture.
CORS is an HTTP header-based protocol that enables resource sharing between different origins. Alongside the HTTP headers, CORS also relies on the browser’s preflight-flight request using the OPTIONS
method for non-simple requests. More on simple and preflight requests later in this article.
Because HTTP headers are the crux of the CORS mechanism, let’s look at these headers and what each of them signifies.
Access-Control-Allow-Origin
The Access-Control-Allow-Origin
response header is perhaps the most important HTTP header set by the CORS mechanism. The value of this header consists of origins that are allowed to access the resources. If this header is not present in the response headers, it means that CORS has not been set up on the server.
If this header is present, its value is checked against the Origin
header of request headers. If the values match, the request will be completed successfully and resources will be shared. Upon mismatch, the browser will respond with a CORS error.
To allow all origins to access the resources in the case of a public API, the Access-Control-Allow-Origin
header can be set to *
on the server. In order to restrict only particular origins to access the resources, the header can be set to the complete domain of the client origin such as https://mywebsite.com
.
Access-Control-Allow-Methods
The Access-Control-Allow-Methods
response header is used to specify the allowed HTTP method or a list of HTTP methods such as GET
, POST
, and PUT
that the server can respond to.
This header is present in the response to pre-flighted requests. If the HTTP method of your request is not present in this list of allowed methods, it will result in a CORS error. This is highly useful when you want to restrict users from modifying the data through POST
, PUT
, PATCH
, or DELETE
requests.
Access-Control-Allow-Headers
The Access-Control-Allow-Headers
response header indicates the list of allowed HTTP headers that your request can have. To support custom headers such as x-auth-token
, you can set up CORS on your server accordingly.
Requests that consist of other headers apart from the allowed headers will result in a CORS error. Similar to the Access-Control-Allow-Methods
header, this header is used in response to pre-flighted requests.
Access-Control-Max-Age
Pre-flighted requests require the browser to first make a request to the server using the OPTIONS
HTTP method. Only after this can the main request be made if it is deemed safe. However, making the OPTIONS
call for each pre-flighted request can be expensive.
To prevent this, the server can respond with the Access-Control-Max-Age
header, allowing the browser to cache the result of pre-flighted requests for a certain amount of time. The value of this header is the amount of time in terms of delta seconds.
More great articles from LogRocket:
- Don't miss a moment with The Replay, a curated newsletter from LogRocket
- Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
- Use React's useEffect to optimize your application's performance
- Switch between multiple versions of Node
- Discover how to use the React children prop with TypeScript
- Explore creating a custom mouse cursor with CSS
- Advisory boards aren’t just for executives. Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Overall, here’s the syntax of how CORS response headers look like:
Access-Control-Allow-Origin: <allowed_origin> | *Access-Control-Allow-Methods: <method> | [<method>]Access-Control-Allow-Headers: <header> | [<header>]Access-Control-Max-Age: <delta-seconds>
Simple requests vs. pre-flighted requests
Requests that do not trigger a CORS preflight fall under the category of simple requests. However, the request has to satisfy some conditions only after it is deemed as a simple request. These conditions are:
- The HTTP method of the request should be one of these:
GET
,POST
, orHEAD
- The request headers should only consist of CORS safe-listed headers such as
Accept
,Accept-Language
,Content-Language
, andContent-Type
apart from the headers automatically set by the user agent - The
Content-Type
header should have only either of these three values:application/x-www-form-urlencoded
,multipart/form-data
, ortext/plain
- No event listeners are registered on the object returned by the
XMLHttpRequest.upload
property if usingXMLHttpRequest
- No
ReadableStream
object should be used in the request
On failing to satisfy either of these conditions, the request is considered to be a pre-flighted request. For such requests, the browser has to first send a request using the OPTIONS
method to the different origin.
This is used to check if the actual request is safe to send to the server. The approval or rejection of the actual request depends on the response headers to the pre-flighted request. If there is a mismatch between these response headers and the main request’s headers, the request is not made.
Enabling CORS
Let’s consider our initial situation where we faced the CORS error. There are multiple ways we could resolve this issue depending on whether we have access to the server on which the resources are hosted. We can narrow it down to two situations:
- You have access to the backend or know the backend developer
- You can manage only the frontend and cannot access the backend server
If you have access to the backend:
Because CORS is just an HTTP header-based mechanism, you can configure the server to respond with appropriate headers in order to enable resource sharing across different origins. Have a look at the CORS headers we discussed above and set the headers accordingly.
For Node.js + Express.js developers, you can install the cors
middleware from npm. Here is a snippet that uses the Express web framework, along with the CORS middleware:
const express = require('express');const cors = require('cors');const app = express();app.use(cors());app.get('/', (req, res) => { res.send('API running with CORS enabled');});app.listen(5000, console.log('Server running on port 5000'));
If you don’t pass an object consisting of CORS configuration, the default configuration will be used, which is equivalent to:
{ "origin": "*", "methods": "GET,HEAD,PUT,PATCH,POST,DELETE", "preflightContinue": false, "optionsSuccessStatus": 204}
Here is how you could configure CORS on your server which will only allow GET
requests from https://yourwebsite.com
with headers Content-Type
and Authorization
with a 10 minutes preflight cache time:
app.use(cors({ origin: 'https://yourwebsite.com', methods: ['GET'], allowedHeaders: ['Content-Type', 'Authorization'], maxAge: 600}));
While this code is specific to Express.js and Node.js, the concept remains the same. Using the programming language and framework of your choice, you can manually set the CORS headers with your responses or create a custom middleware for the same.
If you only have access to the frontend:
Quite often, we may not have access to the backend server. For example, a public API. Due to this, we cannot add headers to the response we receive. However, we could use a proxy server that will add the CORS headers to the proxied request.
The cors-anywhere project is a Node.js reverse proxy that can allow us to do the same. The proxy server is available on https://cors-anywhere.herokuapp.com/
, but you can build your own proxy server by cloning the repository and deploying it on a free platform like Heroku or any other desired platform.
In this method, instead of directly making the request to the server like this:
fetch('https://jsonplaceholder.typicode.com/posts');
Simply append the proxy server’s URL to the start of the API’s URL, like so:
fetch('https://cors-anywhere.herokuapp.com/https://jsonplaceholder.typicode.com/posts');
Conclusion
As we learn to appreciate the same-origin policy for its security against cross-site forgery attacks, CORS does seem to make a lot of sense. While the occurrences of the red CORS error messages in the console aren’t going to magically disappear, you are now equipped with the knowledge to tackle these messages irrespective of whether you work on the frontend or the backend.
- Visit https://logrocket.com/signup/ to get an app ID
- Install LogRocket via npm or script tag.
LogRocket.init()
must be called client-side, not server-side - npm
- Script tag
- (Optional) Install plugins for deeper integrations with your stack:
- Redux middleware
- NgRx middleware
- Vuex plugin
$ npm i --save logrocket// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
Add to your HTML:<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
Get started now