CORS headers are the HTTP headers a server uses to tell the browser which other origins may read its responses. If your frontend at one origin calls an API at another and the console shows a CORS error, the fix is almost always on the server: return the right Access-Control-* headers on the actual response and, for non-simple requests, on the preflight OPTIONS response too. This guide covers each header, when the browser sends a preflight, and how to debug what is really coming back.
TL;DR
- CORS is enforced by the browser, not the server. The server only states its policy through response headers.
Access-Control-Allow-Originis the one header every cross-origin response needs. With cookies or HTTP auth, it must name one exact origin, never*.- A preflight
OPTIONSrequest is triggered by methods other than GET, HEAD or POST, by non-safelisted headers such asAuthorization, or by a JSONContent-Type. - Always inspect the raw response headers before changing code. Most CORS bugs are a missing header on an error page, a redirect, or the preflight itself.
- CORS is not authentication. Protect sensitive data on the backend.
Table of Contents
- What CORS actually is
- The CORS response headers, one by one
- When the browser sends a preflight request
- Credentials, wildcards and Vary
- Fixing the common CORS errors
- Framework notes: Express and beyond
- Security: CORS is not access control
- Our perspective
- Check your headers with Pingfloat
- FAQ
- Sources
What CORS actually is
Browsers apply the same-origin policy: a script on one origin (scheme, host and port together) cannot freely read responses from another. CORS is an HTTP-header based mechanism that lets a server name other origins from which the browser should permit loading resources.
Two consequences trip people up:
- The request often still reaches your server. The browser blocks the script from reading the response. A 200 in your server logs and a CORS error in the console can both be true at once.
- You cannot "disable" CORS from the server. The headers are how your server communicates its policy. A browser reporting a CORS error is doing its job, and the fix is configuring those headers to match the access you intend.
The origin matters down to the detail. https://app.example.com, http://app.example.com and https://app.example.com:8443 are three different origins.
The CORS response headers, one by one
These are the headers a server sends back. The core set is documented as follows.
| Header | What it does | Typical value |
|---|---|---|
Access-Control-Allow-Origin | Which origin may read the response | https://app.example.com or * |
Access-Control-Allow-Methods | HTTP methods allowed (preflight response) | GET, POST, PUT, DELETE |
Access-Control-Allow-Headers | Request headers allowed (preflight response) | Content-Type, Authorization |
Access-Control-Allow-Credentials | Permits cookies and HTTP auth | true |
Access-Control-Expose-Headers | Response headers scripts may read | X-Request-Id, ETag |
Access-Control-Max-Age | How long the preflight result may be cached | 600 |
Access-Control-Allow-Origin
This is the header behind most of the searches and most of the errors. It holds either a single origin or *. The wildcard suits genuinely public resources such as open data or a public font file. For anything else, return the specific origin.
You cannot list several origins in this header. If you need more than one, keep an allowlist on the server, compare the incoming Origin request header against it, and echo back the match. When you do, add Vary: Origin so shared caches do not serve one origin's response to another.
Access-Control-Allow-Methods and Access-Control-Allow-Headers
These answer the preflight. The browser asks, "I want to send a PUT with an Authorization header. Is that allowed?" The server replies with the methods and headers it accepts. If the request header the browser announced is not in your Access-Control-Allow-Headers list, the real request never goes out.
A frequent cause: you added a custom header such as X-Api-Key in the frontend and forgot to add it to the allowed list.
Access-Control-Allow-Credentials
Set to true when the frontend sends cookies or HTTP authentication (credentials: 'include' in fetch). The catch is described in the next section.
Access-Control-Expose-Headers
By default, scripts can read only a small set of response headers. According to the same reference, these include Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified and Pragma. If your frontend needs a pagination header or a request ID, list it in Access-Control-Expose-Headers. Otherwise response.headers.get() returns null even though the header is visible in DevTools.
Access-Control-Max-Age
This controls how long the browser may reuse a preflight answer. Without it, the default is short: MDN's guide lists 5 seconds as the default. Browsers also cap the value. According to cors.dev, the cap is typically 7200 seconds in Chromium-based browsers and 86400 seconds in Firefox. Setting a value of 600 to a few thousand seconds avoids a doubled request count on busy APIs.
Preflight results live in a dedicated cache, not the standard HTTP cache, so do not expect to see them cached in the usual way in the Network tab.

When the browser sends a preflight request
A preflight is an automatic OPTIONS request sent before the real one. MDN's CORS guide says a request becomes "complex", and so needs one, when it:
- uses a method other than GET, HEAD or POST;
- sets non-safelisted headers, such as
Authorizationor customX-headers; - sends a
Content-Typeother thanapplication/x-www-form-urlencoded,multipart/form-dataortext/plain.
That last rule explains why so many teams hit CORS only after switching to JSON: a POST with Content-Type: application/json triggers a preflight, while the same POST as a form does not. The MDN glossary entry describes the preflight as an OPTIONS request carrying request headers that announce the intended method and headers.
What a good preflight exchange looks like
The browser sends:
OPTIONS /api/orders HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type, authorization
The server should answer with a 2xx status and:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600
Vary: Origin
Then the real PUT goes out, and its response must also carry Access-Control-Allow-Origin. Many bugs come from handling only one of the two.

Credentials, wildcards and Vary
The rule that causes the most confusion: * and credentials do not mix. If the client sends cookies or HTTP authentication and the server answers Access-Control-Allow-Origin: *, the browser blocks the response, even though your backend processed the request.
The working pattern:
- Validate the request's
Originagainst an allowlist. - Echo that exact origin in
Access-Control-Allow-Origin. - Send
Access-Control-Allow-Credentials: true. - Add
Vary: Originso caches keep responses for different origins apart.
Never reflect any incoming origin without validating it. That turns a policy into an open door for credentialed requests from any site.
Fixing the common CORS errors
| Symptom in the console | Likely cause | Fix |
|---|---|---|
| Header 'Access-Control-Allow-Origin' missing | The response has no CORS header at all | Add it on the actual response, see MDN's explanation |
| Wildcard not allowed with credentials | * plus cookies | Echo the exact origin, add Vary: Origin |
| Request header not allowed in preflight | Custom header missing from Allow-Headers | Add it to the list |
| Preflight response not OK | OPTIONS returns 401, 404 or 405 | Handle OPTIONS before auth middleware |
| Header reads as null in JS | Not exposed | Add to Access-Control-Expose-Headers |
Check the real response first
Before touching code, look at what the server returns. Three culprits show up again and again:
- Error responses without CORS headers. Your app adds headers on success, but a 500 from a crash or a 504 from a gateway comes from another layer and has none. The browser then reports a CORS error that hides the true problem. Check for a 500 Internal Server Error or a 504 Gateway Timeout behind the message.
- Redirects. A request that gets a 301 redirect, for example from
httptohttpsor adding a trailing slash, can fail when the redirect response lacks CORS headers. Call the final URL directly. - Headers stripped or duplicated by a proxy or CDN. Two
Access-Control-Allow-Originvalues are invalid.
The fastest way to see this is to fetch the URL and read the headers. Our Response Headers Inspector shows what a URL actually returns, and our guide to debugging HTTP headers walks through reading them. Remember that the inspector makes a plain request, so send an Origin header with curl as well when you need to test how the server reacts to a specific origin.
Using a proxy when you don't control the API
If the remote API is not yours and sends no CORS headers, you cannot fix it from the browser. A proxy you control is a legitimate workaround: your frontend calls your own origin, and your server calls the third party. This does not change the third party's policy. It moves the call out of the browser's origin check.
As for "how to enable CORS on Chrome": browser flags and extensions that switch off checks are for local experiments only. They fix nothing for your users, whose browsers will still enforce the policy.
Framework notes: Express and beyond
In Express, the common choice is the cors middleware with an explicit origin option and credentials: true when needed. Mount it before authentication and routes so OPTIONS requests are answered before they hit a 401. In other stacks, the same principle holds: configure CORS once, at the outermost layer that can see the request (application middleware, reverse proxy or API gateway), and avoid setting the same headers in two places.
A quick checklist for any stack:
- Does
OPTIONSreturn 2xx without requiring credentials? - Does the actual response, including errors, carry
Access-Control-Allow-Origin? - Is
Vary: Originpresent when the value changes per request? - Is the allowed-headers list in sync with what the frontend sends?
Security: CORS is not access control
OWASP's testing guide stresses that CORS headers alone are insufficient for protecting sensitive data. The Origin header can be spoofed outside browsers, with curl or a proxy, so any endpoint holding private data needs real authentication and authorization on the backend.
Two related defenses are worth knowing:
- Correct content types. Chrome's Site Isolation and Cross-Origin Read Blocking work alongside CORS. Serve JSON as
application/jsonwithX-Content-Type-Options: nosniffso browsers can tell data from media, which helps even if CORS is misconfigured by accident. - Private network access. Chrome's Private Network Access restricts public websites from sending requests to servers on private networks, requiring explicit preflight permission. If you build tools that talk to routers, internal dashboards or local services, expect this. For the wider picture of why public sites should not reach internal addresses, see this piece on zero trust network access for IT teams, and our own explainer on public versus private IP addresses.
For a broader hardening pass, our web security checklist puts CORS next to the other headers that deserve a review.
Our perspective
We build debugging tools, and the pattern we see in header problems is consistent: people spend hours editing application code when the answer is visible in one raw response. CORS is the clearest example. The error message is generated by the browser, so it describes the symptom, not the layer that failed.
Our advice is to work outside in. Fetch the URL, read the headers, then fetch the OPTIONS response, then compare both with what the frontend sends. Only then touch configuration. Treat * as a deliberate choice for public resources, not a default to paste in when something breaks, and keep Access-Control-Allow-Origin generation in one place so you can reason about it.
The second habit is to monitor, not just test once. A CDN rule change or a deployment can quietly drop headers, and CORS failures then appear as user reports instead of alerts. A scheduled check of the endpoint catches the outage, while the header inspection tells you what changed.
— Andreas
Check your headers with Pingfloat
Pingfloat is a free suite of web tools that needs no installation or account for core use. Start with the Response Headers Inspector to see exactly what your API returns, then use the Website Uptime Monitor to watch the endpoint over time. The rest of the toolkit, from the JWT decoder to the traceroute, is on the Pingfloat home page.
FAQ
How do I stop CORS errors?
You don't turn CORS off. You configure the server to send the right headers for the origin you trust. Start with Access-Control-Allow-Origin on the real response, then make sure the OPTIONS preflight succeeds. If you don't control the server, route requests through your own backend.
What is Access-Control-Allow-Origin used for?
It tells the browser which origin may read a cross-origin response. Use an exact origin for private or credentialed APIs, and * only for public resources. With cookies or HTTP authentication, the wildcard is rejected.
Why is my browser sending an OPTIONS request?
That is a CORS preflight. It fires when you use a method beyond GET, HEAD or POST, add headers like Authorization, or send JSON in the Content-Type. The server must answer it with a 2xx status and the matching allow headers.
Does CORS protect my API from attackers?
No. It only governs what scripts in a browser may read. Anyone can call your API with curl and a forged Origin, so you still need authentication and authorization on the server.
How long is a preflight response cached?
The default is 5 seconds according to MDN. Set Access-Control-Max-Age to raise it, keeping in mind that browsers cap the value, at 7200 seconds in Chromium and 86400 in Firefox according to cors.dev.
Sources
- Cross-Origin Resource Sharing (CORS), MDN
- CORS headers list, cors.dev
- What is a CORS preflight request, cors.dev
- Preflight request glossary, MDN
- Access-Control-Expose-Headers, MDN
- Reason: CORS header Access-Control-Allow-Origin missing, MDN
- OWASP: Testing Cross Origin Resource Sharing
- Site Isolation for web developers, Chrome for Developers
- Private Network Access update, Chrome for Developers
