Every CORS Header Explained (With Real Request/Response Examples)

This is the page I keep coming back to when I need to remember the exact syntax or behavior of a CORS header. I’m putting it all in one place so you don’t have to hunt through MDN and Stack Overflow.

Response Headers (What Your Server Sends)#

These are the headers your API server needs to send. The browser reads these to decide whether to allow the cross-origin request.

Access-Control-Allow-Origin#

The single most important CORS header. Without it, nothing works.

Access-Control-Allow-Origin: https://myapp.com

Three valid values:

  • A specific origin like https://myapp.com — only that origin can access the API
  • * — any origin can access (not compatible with credentials)
  • Nothing — the request is blocked

You cannot list multiple origins in a single header. If you need to allow multiple origins, your server needs to check the request’s Origin header and return the matching one:

const allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
const requestOrigin = req.headers.origin;
if (allowedOrigins.includes(requestOrigin)) {
  res.setHeader('Access-Control-Allow-Origin', requestOrigin);
}

Access-Control-Allow-Methods#

Which HTTP methods the actual request is allowed to use. Only used in preflight responses.

Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS

List every method your API accepts. Don’t use * here if you can help it — be explicit.

Access-Control-Allow-Headers#

Which request headers the actual request is allowed to include. Only used in preflight responses.

Access-Control-Allow-Headers: Content-Type, Authorization, X-Custom-Header

Common headers to include:

  • Content-Type — required if sending JSON
  • Authorization — required for bearer tokens
  • X-Requested-With — used by some frameworks (jQuery, Axios)
  • Accept — rarely required but sometimes

Access-Control-Allow-Credentials#

Whether the response can include credentials (cookies, authorization headers, TLS certificates).

Access-Control-Allow-Credentials: true

Important rules:

  • Must be true or false (it’s not a list)
  • Cannot be true when Allow-Origin is *
  • The request must also be sent with credentials: 'include' (fetch) or withCredentials: true (XMLHttpRequest)
  • When using credentials, cookies are sent with every request, even preflight

Access-Control-Expose-Headers#

By default, the browser only exposes a few “CORS-safelisted” response headers to JavaScript: Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma. Any other headers are visible in the network tab but not accessible via response.headers.

Access-Control-Expose-Headers: X-Total-Count, X-Page-Number, X-Request-Id

If your API returns custom headers that your frontend needs to read (like pagination info), you MUST list them here.

This catches a lot of people off guard. They can see the header in DevTools but response.headers.get('X-Total-Count') returns null. That’s because the browser is hiding it from JavaScript. Add it to Expose-Headers and it works.

Access-Control-Max-Age#

How long the preflight result can be cached, in seconds.

Access-Control-Max-Age: 86400

Without this, the browser sends a preflight OPTIONS request before every single API call. With Max-Age: 86400, it caches the result for 24 hours.

Set this to a reasonable value. 86400 (24 hours) is a good default. Some people use longer (604800 = 7 days) if their CORS config rarely changes.

Access-Control-Allow-Private-Network#

Chrome’s Private Network Access feature requires this header when a public website accesses a private network resource (localhost, 192.168.x.x, etc.).

Access-Control-Allow-Private-Network: true

Request Headers (What the Browser Sends Automatically)#

You don’t set these manually. The browser adds them. But understanding them helps with debugging.

Origin#

Sent with every cross-origin request (and some same-origin requests).

Origin: https://myapp.com

This tells the server where the request is coming from. The server uses this to decide what to put in Access-Control-Allow-Origin.

Access-Control-Request-Method#

Sent only in preflight (OPTIONS) requests. Tells the server what method the actual request will use.

Access-Control-Request-Method: PUT

Access-Control-Request-Headers#

Sent only in preflight requests. Tells the server what custom headers the actual request will include.

Access-Control-Request-Headers: Content-Type, Authorization

Complete Real-World Example#

Let’s say your React app at https://myapp.com wants to PUT a user profile update to your API at https://api.example.com/users/123 with a JSON body and a JWT token.

Step 1: The browser sends a preflight

OPTIONS /users/123 HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization

The browser asks: “Can I send a PUT request with JSON content and an Authorization header?”

Step 2: The server responds

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400

The server says: “Yes, you can. Here are the rules.”

Step 3: The browser sends the actual request

PUT /users/123 HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

{"name": "Updated Name", "email": "[email protected]"}

Step 4: The server responds

HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-Request-Id
X-Request-Id: abc-123

{"id": 123, "name": "Updated Name", "email": "[email protected]"}

The browser sees the correct headers and allows your JavaScript to read the response. Done.

Verify Your CORS#

Use headertest.com to check your CORS configuration.