The browser’s Same-Origin Policy is the guardian of your data, but CORS headers are the bouncer that lets trusted guests through.

Imagine a web application running on https://www.example.com. This app needs to fetch data from an API hosted on https://api.anotherservice.com. By default, the browser’s Same-Origin Policy (SOP) would block this request because the origin of the web app (https://www.example.com) is different from the origin of the API (https://api.anotherservice.com). This is where Cross-Origin Resource Sharing (CORS) comes in. CORS is a mechanism that uses additional HTTP headers to tell a browser that a web application, running at one origin, is permitted to request resources from a server at a different origin.

Let’s see this in action.

Scenario: A simple JavaScript fetch request from one origin to another.

https://www.frontend.com/index.html

<!DOCTYPE html>
<html>
<head>
    <title>CORS Example</title>
</head>
<body>
    <button id="fetchData">Fetch Data</button>
    <div id="result"></div>

    <script>
        document.getElementById('fetchData').addEventListener('click', () => {
            fetch('https://www.backend.com/data')
                .then(response => {
                    if (!response.ok) {
                        throw new Error(`HTTP error! status: ${response.status}`);
                    }
                    return response.json();
                })
                .then(data => {
                    document.getElementById('result').innerText = JSON.stringify(data);
                })
                .catch(error => {
                    document.getElementById('result').innerText = `Error: ${error.message}`;
                    console.error("Fetch error:", error);
                });
        });
    </script>
</body>
</html>

If https://www.backend.com doesn’t send the correct CORS headers, the browser will block the fetch request and you’ll see an error in the console similar to:

Access to fetch at 'https://www.backend.com/data' from origin 'https://www.frontend.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

The Server’s Role: Adding CORS Headers

For the fetch request to succeed, the server at https://www.backend.com needs to include specific CORS headers in its response. The most fundamental header is Access-Control-Allow-Origin.

Example Server Configuration (Node.js with Express)

Here’s how you might configure CORS on the backend server using the cors middleware:

const express = require('express');
const cors = require('cors'); // npm install cors
const app = express();

// Configure CORS to allow requests from www.frontend.com
const corsOptions = {
  origin: 'https://www.frontend.com', // Specify the allowed origin
  optionsSuccessStatus: 200 // Some legacy browsers (IE11, various SmartTVs) choke on 204
};

app.use(cors(corsOptions));

app.get('/data', (req, res) => {
  res.json({ message: 'This is your data!', timestamp: new Date() });
});

app.listen(3000, () => {
  console.log('API server listening on port 3000');
});

When the browser makes a GET request to https://www.backend.com/data from https://www.frontend.com, the server responds with:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.frontend.com
Content-Type: application/json; charset=utf-8
... other headers ...

{"message":"This is your data!","timestamp":"2023-10-27T10:00:00.000Z"}

The browser sees the Access-Control-Allow-Origin: https://www.frontend.com header, matches it with the origin of the page making the request (https://www.frontend.com), and allows the JavaScript code to access the response.

The CORS Mechanism: Preflight and Simple Requests

CORS distinguishes between "simple requests" and "non-simple requests."

  • Simple Requests: These are requests that meet specific criteria:

    • The request method is GET, HEAD, or POST.
    • The only allowed headers are Accept, Accept-Language, Content-Language, Content-Type (with a value of application/x-www-form-urlencoded, multipart/form-data, or text/plain). For simple requests, the browser directly sends the request to the server. If the server includes the correct Access-Control-Allow-Origin header, the browser allows the response.
  • Non-Simple Requests (including PUT, DELETE, custom headers, etc.): For these, the browser first sends a "preflight" request. This is an OPTIONS request to the URL of the resource, asking for permission before sending the actual request. The preflight request includes headers like Access-Control-Request-Method and Access-Control-Request-Headers.

    The server must respond to the preflight OPTIONS request with:

    • Access-Control-Allow-Origin: The origin that is allowed to make the request.
    • Access-Control-Allow-Methods: A comma-separated list of HTTP methods allowed for the resource (e.g., GET, POST, PUT, DELETE).
    • Access-Control-Allow-Headers: A comma-separated list of headers that the client is allowed to send in the actual request (e.g., Content-Type, Authorization).

    If the preflight response indicates that the actual request is allowed, the browser then sends the actual request.

Key CORS Headers Explained:

  • Access-Control-Allow-Origin: The most important one. It specifies which origins are permitted to access the resource. It can be a specific origin (e.g., https://www.frontend.com) or a wildcard * (though using * is generally discouraged for security reasons unless the resource is public and doesn’t involve sensitive data).
  • Access-Control-Allow-Methods: Used in preflight responses to indicate which HTTP methods are allowed for the resource.
  • Access-Control-Allow-Headers: Used in preflight responses to specify which HTTP headers the client can use in the actual request.
  • Access-Control-Allow-Credentials: If set to true, it indicates that the server allows credentials (like cookies or HTTP authentication) to be sent with the request. If this is true, Access-Control-Allow-Origin cannot be *.
  • Access-Control-Expose-Headers: By default, JavaScript can only access a limited set of "CORS-safelisted" response headers. This header allows the server to specify other headers that should be accessible to the browser’s JavaScript.
  • Access-Control-Max-Age: Indicates how long the results of a preflight request can be cached by the browser, in seconds. This reduces the number of OPTIONS requests.

The Mental Model:

Think of the browser as a strict security guard for your web page’s origin. When your page (Origin A) wants to talk to a different server (Origin B), the guard (browser) asks Origin B, "Hey, is Origin A allowed to talk to you, and if so, what can they do?" Origin B sends back a note (CORS headers) saying, "Yes, Origin A is allowed to GET and POST and send Content-Type headers." The guard checks the note against Origin A’s request. If it matches, the guard lets the conversation proceed. If not, the guard blocks it and tells Origin A it’s not allowed.

The Access-Control-Allow-Credentials: true header is particularly nuanced. When set on the server, it signals to the browser that it’s okay to include cookies or authentication headers in the request to the cross-origin server. However, the browser will only send credentials if the Access-Control-Allow-Origin header is a specific origin (e.g., https://www.frontend.com), not if it’s the wildcard *. This is a crucial security measure.

Understanding the interplay between the browser’s SOP and the server’s CORS headers is key to building secure and functional web applications that communicate across different origins.

The next hurdle you’ll likely encounter is dealing with authenticated requests across origins, which brings Access-Control-Allow-Credentials and cookie handling into sharp focus.

Want structured learning?

Take the full Computer Networking course →