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.comThree 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, OPTIONSList 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-HeaderCommon headers to include:
Content-Type— required if sending JSONAuthorization— required for bearer tokensX-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: trueImportant rules:
- Must be
trueorfalse(it’s not a list) - Cannot be
truewhenAllow-Originis* - The request must also be sent with
credentials: 'include'(fetch) orwithCredentials: 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-IdIf 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: 86400Without 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: trueRequest 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.comThis 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: PUTAccess-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, AuthorizationComplete 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, AuthorizationThe 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: 86400The 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.