CORS (cross-origin resource sharing) is how a server tells the browser that JavaScript from another origin may read its responses. The server answers with Access-Control-Allow-Origin and related headers. Requests using other methods, custom headers or JSON bodies first send an OPTIONS preflight. The fix always belongs on the server you are calling, never the page.
| Header | What it allows |
|---|---|
| Access-Control-Allow-Origin | Which origin may read the response (one origin or *) |
| Access-Control-Allow-Methods | Methods allowed after a preflight |
| Access-Control-Allow-Headers | Request headers allowed after a preflight |
| Access-Control-Allow-Credentials | Cookies and auth sent with the request (value must be true) |
| Access-Control-Expose-Headers | Response headers that script may read |
| Access-Control-Max-Age | Seconds a preflight answer may be cached |
Updated
A CORS error looks like your code is broken, yet the request usually reached the server just fine. The browser simply refused to hand the answer to your script. The simulator below lets you describe a request and a server setup, then tells you whether a preflight fires, whether the browser lets the response through, and which console message you would see.
Describe a request and the server's CORS headers to see whether a preflight is sent and whether the browser lets your script read the response.
Message wording varies by browser. These are the reasons as Firefox names them on MDN; Chrome says the same things in longer sentences.
| Console reason | What it means | Fix on the server |
|---|---|---|
| CORS header 'Access-Control-Allow-Origin' missing | The response carried no allow-origin header at all | Add the header to every response, including errors |
| CORS header 'Access-Control-Allow-Origin' does not match | The header names a different origin | Echo the caller's origin from your allowlist |
| Credential is not supported if the CORS header 'Access-Control-Allow-Origin' is '*' | Cookies were sent but the server answered with * | Return the exact origin and Allow-Credentials: true |
| expected 'true' in CORS header 'Access-Control-Allow-Credentials' | Credentials sent but the flag is absent | Send Access-Control-Allow-Credentials: true |
| Did not find method in CORS header 'Access-Control-Allow-Methods' | Preflight did not approve the method | List PUT, PATCH or DELETE in Allow-Methods |
| missing token in CORS header 'Access-Control-Allow-Headers' from CORS preflight channel | A request header such as Authorization was not approved | List it in Allow-Headers |
| CORS preflight channel did not succeed | OPTIONS failed, redirected or returned an error | Answer OPTIONS with 204 and the headers |
| Multiple CORS header 'Access-Control-Allow-Origin' not allowed | Two layers (app and proxy) both added the header | Set it in one place only |
Echoes the origin only when it is on the list, answers preflights with 204, and adds Vary so caches keep origins apart.
const ALLOWED = new Set(["https://app.example.com", "http://localhost:5173"]);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin && ALLOWED.has(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader("Vary", "Origin");
}
if (req.method === "OPTIONS") {
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.setHeader("Access-Control-Max-Age", "600");
return res.sendStatus(204);
}
next();
});A public, cookie-free endpoint can use the wildcard. Export OPTIONS so the preflight gets its own answer.
// app/api/data/route.js
const CORS = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
export function OPTIONS() {
return new Response(null, { status: 204, headers: CORS });
}
export async function POST(request) {
const body = await request.json();
return Response.json({ received: body }, { headers: CORS });
}The always flag keeps the header on error responses too, so a 500 still shows its real status instead of a CORS error.
location /api/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://app.example.com";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE";
add_header Access-Control-Allow-Headers "Content-Type, Authorization";
add_header Access-Control-Max-Age 600;
return 204;
}
add_header Access-Control-Allow-Origin "https://app.example.com" always;
proxy_pass http://127.0.0.1:3000;
}credentials: "include" is what turns on the stricter credentials rules on the server.
const res = await fetch("https://api.example.com/me", {
method: "GET",
credentials: "include",
});
if (!res.ok) throw new Error("HTTP " + res.status);
const me = await res.json();
An origin is the scheme, host and port together. https://app.example.com and https://api.example.com are different origins, and so are http and https versions of one domain. By default a script may send a request to another origin but may not read what comes back.
CORS is the opt-in. The API lists which origins it trusts and the browser enforces that list on the visitor's behalf. That is why the header must come from the server you call.
A plain GET, or a POST from an ordinary form, goes straight out with an Origin header. The browser then checks the reply for an allowed origin before giving it to your code.
Anything beyond that first sends an OPTIONS request carrying Access-Control-Request-Method and Access-Control-Request-Headers. Only if the server approves both does the real request go out. Access-Control-Max-Age lets the browser remember the approval, though browsers cap how long.
For security, your JavaScript only learns that the fetch failed, with a TypeError and no detail. The real reason appears in the browser console, and it names the header that was missing or wrong. Match it against the table of messages below.
CORS is enforced only by browsers, to stop one site's scripts from reading another site's data with your cookies. Tools like curl are not running untrusted page code, so they ignore it. The server response was identical; only the browser decided to withhold it.
Any method other than GET, HEAD or POST, any header outside the safelisted few (Accept, Accept-Language, Content-Language, Content-Type, Range), or a Content-Type other than form encoding, multipart or text/plain. Sending application/json is the most common trigger.
No. Setting Access-Control headers on your request does nothing, and mode: no-cors gives you an opaque response your script cannot read. Either the API adds the headers, or your own server fetches the data and passes it on.
No. When a request carries credentials, the browser rejects a wildcard origin and requires the exact origin plus Access-Control-Allow-Credentials: true. The same applies to wildcards in the methods, headers and expose headers lists.
Cross-origin script only sees a short list of safe response headers by default. List any others in Access-Control-Expose-Headers, for example X-Request-Id, and they become readable.
Related: HTTP status codes, Next.js API routes, Webhook vs API, OAuth explained, Website authentication guide.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.