How to Debug CORS Errors: A Practical Field Guide
CORS is enforced only by the browser, and most fixes live on the server. Read the failing preflight and the console error together, and the right Access-Control-Allow-* header stops being a guess.

You wrote a fetch, it works in Postman, and the browser console lights up red: blocked by CORS policy. The reflex is to assume the request failed. It usually did not. Cross-Origin Resource Sharing is a browser-enforced rule, and a CORS error means the response came back but the browser will not hand it to your JavaScript because the server did not say your origin is allowed.
That single reframing changes the whole debugging loop. You stop poking at the client and start reading what the server returned. This guide covers what the error actually means, when the browser inserts a preflight OPTIONS request, how to read the failure across the Console and Network tabs, and the exact server-side header to add for each common misconfiguration. The reference throughout is MDN's CORS documentation, which enumerates the rules and the error strings precisely.
What does a CORS error actually mean?
A CORS error means the browser blocked a cross-origin response because the server did not return the headers authorizing your origin. The request often reached the server and ran; the browser simply withholds the response from your script. CORS is enforced only by browsers, so the same call succeeds in curl or Postman. Most fixes are server-side: send the correct Access-Control-Allow headers.
The same-origin policy is the default: a page at https://app.example.com cannot read a response from https://api.other.com unless that server opts in. CORS is the opt-in mechanism. The server grants access by returning an Access-Control-Allow-Origin header naming the requesting origin (or *). No header, no access. The browser blocks the read and prints a reason in the console.
This is why your API works in Postman and curl but not the browser: command-line clients do not implement the same-origin policy, so they never look for the header. That difference is the single most common source of confusion. The server is fine; the browser is doing its job. MDN states the consequence plainly: most CORS errors can only be resolved on the server, because the server controls whether cross-origin access is allowed.
Simple requests vs. preflighted requests
The browser splits cross-origin requests into two kinds, and knowing which one you are making tells you where to look. A request stays a simple request — no preflight — only when it uses GET, HEAD, or POST and sets nothing outside the CORS-safelisted request headers: Accept, Accept-Language, Content-Language, Content-Type, and Range. Anything else upgrades it to a preflighted request.
The trap most engineers hit is Content-Type. A POST stays simple only when its Content-Type is application/x-www-form-urlencoded, multipart/form-data, or text/plain. The moment you send a JSON body — application/json — the request is downgraded to a preflight. A custom Authorization header does the same thing. So your everyday JSON API call almost always triggers an OPTIONS preflight before the real request goes out.
| Feature | Request attribute | Stays simple (no preflight) | Triggers preflight |
|---|---|---|---|
| HTTP method | Verb used | GET, HEAD, POST | PUT, PATCH, DELETE, etc. |
| Content-Type | Body media type | form-urlencoded, multipart, text/plain | application/json and others |
| Custom headers | Headers set by code | Only CORS-safelisted | Authorization, X-* custom headers |
A preflight is an automatic OPTIONS request the browser sends first, carrying Access-Control-Request-Method and Access-Control-Request-Headers to ask: will you accept this verb and these headers? The server must answer with matching Access-Control-Allow-Methods and Access-Control-Allow-Headers and a success status. If it does, the browser caches that answer for the duration of Access-Control-Max-Age — MDN documents a default of 5 seconds, with each browser enforcing its own internal maximum that overrides larger values — then sends the real request. If the preflight fails, the real request never leaves the browser.
Read it in the console and the network tab
Debug CORS with both DevTools panels open, because each tells you half the story. The Console gives you the reason string. The Network tab gives you the evidence. Chrome prints messages like No 'Access-Control-Allow-Origin' header is present on the requested resource; Firefox prints a named reason from MDN's list — CORSMissingAllowOrigin, CORSNotSupportingCredentials, CORSMethodNotFound, CORSMissingAllowHeaderFromPreflight, and about a dozen more. The reason string is not noise. It maps directly to the missing header.
Then switch to Network. For a preflighted request you will see two rows: a failed OPTIONS sitting just above the real request. Click the OPTIONS row and read its response headers. Is Access-Control-Allow-Origin present? Does it match your origin exactly? Does Access-Control-Allow-Headers include the header you sent? The absent or mismatched header is the bug. The request may show as (failed) or CORS error instead of a normal status code.
Fixes by server, by error
Because the fix is server-side, the work is adding the right header to the response for the route the browser calls. For a missing Access-Control-Allow-Origin, MDN gives the canonical snippets. The wildcard * works only for non-credentialed requests; name the explicit origin when credentials are involved.
# --- Apache: missing Access-Control-Allow-Origin ---
# Header set Access-Control-Allow-Origin "https://app.example.com"
# --- Nginx: allow a named origin (use the explicit origin, not * with credentials) ---
add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
# --- Nginx: answer the preflight for a JSON PUT/PATCH/DELETE ---
location /api/ {
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
add_header 'Access-Control-Max-Age' 86400 always;
return 204;
}
# ...proxy_pass to your app
}
# --- Credentialed request: explicit origin + the only valid credentials value ---
add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
add_header 'Access-Control-Allow-Credentials' 'true' always; # 'true' is the ONLY valid valueThree rules cover most of the named errors. First, for CORSMethodNotFound on a verb like PUT, PATCH, or DELETE, add it to Access-Control-Allow-Methods — note that GET, HEAD, and POST are always permitted as CORS-safelisted methods even if you omit them, so a missing method only matters for the other verbs. Second, for CORSMissingAllowHeaderFromPreflight, the preflight response must include Access-Control-Allow-Headers covering whatever the request listed in Access-Control-Request-Headers (commonly Authorization or Content-Type). Third, for credentialed requests, never combine a wildcard with credentials.
If you do not control the server at all, you have two escapes MDN notes: route the call through a proxy you do control so the browser talks to a same-origin endpoint, or restructure the request as a simple request so it skips preflight. Neither is a substitute for fixing the upstream headers, but both unblock you.
Make the failure agent-readable
Every CORS guide ends here: open DevTools, read two panels, add a header. That loop assumes a human is sitting in front of the browser cross-referencing the console reason against the preflight response. The evidence is split across two places and exists only in that one DevTools session. Paste the console line into a chat with an AI agent and you have thrown away the half that matters — the preflight response headers that name the missing Allow-*.
This is the wedge BugMojo is built on. The browser extension captures the failing exchange as one artifact: the OPTIONS preflight with its response headers, the matching console CORS error string, and the originating fetch that triggered it, lined up together. The MCP server hands that bundle to an AI coding agent — Claude Code, Cursor — as structured evidence. The agent sees the preflight, sees that Access-Control-Allow-Origin is absent, sees the request that needed it, and names the exact server-side fix instead of guessing from a pasted error line.
| Feature | Browser DevTools | Error monitoring (Sentry) | BugMojo |
|---|---|---|---|
| Live, full DevTools network/console depth | ✓ | partial | partial |
| Mature production error aggregation & alerting | — | ✓ | early |
| Preflight OPTIONS + console error captured as one artifact | manual | — | ✓ |
| Shareable one-click capture (no copy-paste) | — | partial | ✓ |
| Failing exchange exposed to an AI agent over MCP | — | — | ✓ |
| Agent names the server-side fix from the evidence | — | — | ✓ |
A CORS error is the server's missing header echoed back by the browser. Read the preflight next to the console reason and the fix names itself.BugMojo engineering
Install the free BugMojo extension to capture the OPTIONS preflight, the console CORS reason, and the originating fetch as one shareable artifact — then let an AI agent name the server-side fix over MCP. No project setup required.
Install the extensionFrequently asked questions
Frequently asked questions
Sources
- Cross-Origin Resource Sharing (CORS) — simple requests, CORS-safelisted methods and headers, preflight triggers, Access-Control-Max-Age default, and credentials rules — MDN Web Docs (2025)
- CORS errors — the named browser error strings (CORSMissingAllowOrigin, CORSNotSupportingCredentials, CORSMethodNotFound, etc.) and that most are resolved on the server — MDN Web Docs (2025)
- Reason: CORS header 'Access-Control-Allow-Origin' missing — cause and server fixes with Apache and Nginx config snippets — MDN Web Docs (2025)
- Access-Control-Allow-Credentials header — only valid value is true; omit rather than set false; cannot combine with a wildcard origin — MDN Web Docs (2025)
- Access-Control-Allow-Methods header — GET, HEAD, and POST are always allowed as CORS-safelisted methods regardless of the header — MDN Web Docs (2025)
- Access-Control-Allow-Headers header — required in the preflight response when the request carries Access-Control-Request-Headers — MDN Web Docs (2025)
Get bug-tracking insights, weekly.
Engineering deep-dives, QA playbooks, and honest tool comparisons. No spam — unsubscribe in one click.

