What is CORS (Cross Origin Resource Sharing): Purpose and Workflow
To understand what CORS is, let’s use a simple app: a nodejs app that returns as a response a simple message, and then we fetch this message and display it in a react app. We’ll use Chrome Dev Tools to help us get the CORS flow.
The CORS error
This is the node.js code:
const express = require('express');
const app = express();
app.use(express.json());
app.get('/', (req, res) => {
res.send('Hello from Express, cors demo!');
});
const PORT = process.env.PORT || 8000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
In React, we have a simple component that uses axios to fetch the message from the server and displays:
import React from "react";
import { useState, useEffect } from "react";
import axios from "axios";
const NodeMessage = () => {
const [message, setMessage] = useState('');
useEffect(() => {
axios.get('http://localhost:8000')
.then(response => {
console.log("response, ", response)
setMessage(response.data);
})
.catch(error => {
console.error('Error fetching data:', error);
});
}, []);
return(
<>
<h3>Message from the server</h3>
<p>{message}</p>
</>
)
}
export default NodeMessage;
Run both the server and react, we’ll have the following message displayed in the browser by the server : `Hello from Express, cors demo!`, and react, you’ll see the following:
As you see, the message was not retreived from the server and using chrome dev tools, in the Network tab. there is an error message : CORS error
and. Our code works fine, however, we’re unable to get the response from the server. If we further check the request and response headers:
We’ll get back later to these request and response headers. Now let’s see how to fix this error and then we’ll talk about what cors is.
Let’s just add the cors
package to our nodejs code, and the new code becomes:
const express = require('express');
// we added this
const cors = require('cors');
const app = express();
app.use(express.json());
// and this
app.use(cors());
app.get('/', (req, res) => {
res.send('Hello from Express, cors demo!');
});
const PORT = process.env.PORT || 8000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
When we refresh react app now, we’ll get the following:
From the screenshot, the message was fetched and displayed, and the cors
error disappeared, and we see a 200 status code in the request we made. If we inspect the request and response headers:
if we compare the request and response headers between the cors error and with the successful response, we notice that the request headers are the same, and in the response headers from the server, in the successful response, we notice the header Access-Control-Allow-Origin
and its value is *
, whch was not present when we had the cors error. Let’s see below what this header means.
CORS and Same Origin Policy
The error that we have encountered without setting CORS is due to the fact that the browser by default uses the Same Origin Policy (SOP), and CORS came to extend this Same Origin Policy. SOP is a security mechanism implemented by the browsers so that only scripts of the same domain can access another page of the same domain. In other words, it prevents web pages from accessing resources on different origins (An origin is a combination of schema (HTTP or HTTPS), domain, and port number (example: http://example.com:81)) for the purpose of protecting user’s data and mitigating malicious attacks (such as CSRF and injecting some scripts in other domains pages).
CORS allows the server to relax the default restrictions of the server so that domains can share resources with other domains, the server can therefore select which origins can access its resources through HTTP headers. CORS is implemented through a combination of headers exchanged between the browser and the server. The server includes headers such as `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods` and `Access-Control-Allow-Credentials` in its responses to indicate which origins can access its resources, which HTTP methods are permitted for cross origin requests, and which credentials are permitted for cross origin requests (such as access tokens, etc). In our example, we received only `Access-Control-Allow-Origin`, and it was *
``` which means all origins are allowed to access the server, and this practice is not recommended for production, and you should only specify the origins that need to request your server. Depending on the framework, follow their documentation about how to setup cors. In nodejs for instance, in order to specify specific origins and only allow http://localhost:5173
, we can update our code as following:
const express = require('express');
// we added this
const cors = require('cors');
const app = express();
app.use(express.json());
// Allowing specific origins
const allowedOrigins = ['http://localhost:5173'];
const corsOptions = {
origin: function (origin, callback) {
if (allowedOrigins.indexOf(origin) !== -1 || !origin) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
};
app.use(cors(corsOptions));
app.get('/', (req, res) => {
res.send('Hello from Express, cors demo!');
});
const PORT = process.env.PORT || 8000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
When a webpage makes a request to a different origin via javascript, the browser restricts such requests for security reasons, and CORS provides a way for servers to specify which origins are allowed to access their resources. To enable CORS, server-side configuration is necessary.
Here is how CORS works:
- For simple requests (GET, HEAD, POST, and some content types), the browser sends an HTTP request with an “Origin” header indicating the origin of the requesting site. The server then responds with `Access-Control-Allow-Origin` , specifying which origins are allowed to access its resources. If the origin matches the allowed origins, the browser then allows the response to be accessed by the requesting site’s javascript. In our example, Access-Control-Allow-Origin: localhost:5173, which we specified in our allowed origins lists, other origins will not be able to access the server and the CORS error will occur.
Up to now, and our example was in the case of a GET
request, the flow of CORS is a bit different in the case of what we call non-simple reuquests:
- Non-simple requests include PUT, and DELETE methods that change the state of the resources on the server, POST when used with custom headers or with credentials (such as authorization headers), is also considered in this case a non-simple request. The browser sends a preflight request (A preflight request is an HTTP
OPTIONS
request) to the server to check if the current request is safe, the server responds with headers indicating which methods, headers, and origins are allowed. If the preflight request is successful, the actual request is then sent. The preflight request was created for the purpose of protecting user’s data on old servers that do not support CORS, and to prevent dangerous operations changing the state such deleting or updating something on the server. If there is no response from the server after rececing the preflight response, it means CORS is not supported, and the browser will drop the request. The preflight can be cached, so that only the first request will make the preflight request and subsequent requests will not.
Let’s update our example to use a non-simple request, and let’s use PUT
for this demo:
We add the following routes and value to test updating using PUT
:
const express = require('express');
// we added this
const cors = require('cors');
const app = express();
app.use(express.json());
// Allowing specific origins
const allowedOrigins = ['http://localhost:5173'];
const corsOptions = {
origin: function (origin, callback) {
if (allowedOrigins.indexOf(origin) !== -1 || !origin) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
};
app.use(cors(corsOptions));
// The value that we'll update from the client:
let preflightValue = { value: 'Some initial value' };
app.get('/', (req, res) => {
res.send('Hello from Express, cors demo!');
});
// Route to handle GET request to retrieve the current value
app.get('/value', (req, res) => {
res.json(preflightValue);
});
// Route to handle PUT request to update the value
app.put('/value', (req, res) => {
const newValue = req.body.value;
preflightValue.value = newValue;
res.json({ message: 'Value updated successfully', newValue: preflightValue.value });
});
const PORT = process.env.PORT || 8000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
and in react, we create this component:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function PreflightDemo() {
const [value, setValue] = useState('');
const [PreflightValue, setPreflightValue] = useState('');
// Fetch the current value from the server when the component mounts
useEffect(() => {
axios.get('http://127.0.0.1:8000/value')
.then(response => {
setPreflightValue(response.data.value);
})
.catch(error => {
console.error('There was an error fetching the data!', error);
});
}, []);
// Handle the input change
const handleChange = (e) => {
setValue(e.target.value);
};
// Handle the form submission and send a PUT request
const handleSubmit = (e) => {
e.preventDefault();
axios.put('http://127.0.0.1:8000/value', { value })
.then(response => {
setPreflightValue(response.data.newValue);
alert('Value updated successfully');
})
.catch(error => {
console.error('There was an error updating the value!', error);
});
};
return (
<div>
<h1>Update Server Value</h1>
<p>Current server value: {PreflightValue}</p>
<form onSubmit={handleSubmit}>
<input
type="text"
value={value}
onChange={handleChange}
placeholder="Enter new value"
/>
<button type="submit">Update Value</button>
</form>
</div>
);
}
export default PreflightDemo;
it looks like this :
Now, let’s try to update the initial value with “preflight test!”:
Now we see 2 requests:
- Preflight request first
- The actual request to update the value
Let’s check now the request and response headers in each of the two requests:
In the preflight request headers, we notice the `Access-Control-Request-Headers`, `Access-Control-Request-Method`, which is in our case a PUT
request, the preflight request is checking for access to the server for the PUT
method. In the response header of the preflight, we see the `Access-Control-Allow-Headers` of type content-type, `Access-Control-Allow-Methods `: GET
, HEAD
, PATCH
, POST
, PUT
and DELETE
, and finally, the `Access-Control-Allow-Origin`: https://localhost:5173. You can customize your cors headers and allowed methods as you wish, just follow the documentation of the framework you’re using 😉😉.
Here are the request headers in our PUT
request:
Just some headers saying what type of encoding the browser accepts, and the origin header.
In the response headers of our PUT
request, we get the origin that is allowed in the `Access-Control-Allow-Origin`:
That’s it for this article 🎊 🎉😊😄😉 , I hope it is now clear for you what CORS is, and how CORS works. For more details, You can check out mdn official cors documentation.