Decoding Browser CORS Error Messages
A CORS failure surfaces to a developer as a single red line in the browser console — but that line is the last step of a multi-stage algorithm, and its wording is chosen by the browser vendor, not the specification. Chrome, Firefox, and Safari describe the same underlying failure with three different sentences, and each embeds a short reason phrase that is the only reliable signal of what actually broke. Reading that phrase correctly is the difference between fixing a missing header in five minutes and rewriting a working server config for an hour.
This page is part of Cross-Origin Debugging & Error Diagnosis, which covers the end-to-end workflow of reproducing, inspecting, and resolving cross-origin failures. Here the focus is narrow and practical: a decoder that maps each exact console string to the WHATWG Fetch algorithm step it represents, the network layer where the fault lives, and the concrete remediation.
Why the Wording Matters More Than the Sentence
The WHATWG Fetch Standard defines a single “CORS check” algorithm and a single “CORS-preflight fetch” algorithm. When either fails, the spec returns a network error — an opaque failure with no readable status, headers, or body. Browsers are free to render that network error however they like in the console. The result is that the sentence structure varies by vendor, but each vendor appends a machine-generated reason phrase derived from the exact failing branch:
- Chrome:
... has been blocked by CORS policy: <reason> - Firefox:
Cross-Origin Request Blocked: ... (Reason: <reason>) - Safari: a terse
Origin ... is not allowed by Access-Control-Allow-OriginorPreflight response is not successfulline in the console, with fuller detail in the Web Inspector Network tab.
The reason phrase is the payload. Two failures with identical Chrome sentences but different reason clauses have completely different fixes; two failures with different sentences (one Chrome, one Firefox) but equivalent reason clauses have the same fix. Decode by reason, never by prose.
Spec Anchor: Where Each Error Is Raised
Two algorithm locations produce nearly every CORS console error:
- The CORS check (WHATWG Fetch §4.9). After a response is received, the browser compares the response’s
Access-Control-Allow-Originvalue against the request’sOrigin. Absence, mismatch, or a wildcard used with credentials all fail this check. - The CORS-preflight fetch (WHATWG Fetch §4.8). Before a non-simple request, the browser sends an
OPTIONSrequest and validates the response’sAccess-Control-Allow-MethodsandAccess-Control-Allow-Headers. A failure here blocks the request before the real method is ever sent.
A third, distinct class is the failed fetch — the response never arrived in a usable form (TLS handshake failure, connection refused, mixed content, or the server never answered OPTIONS). Firefox names this precisely; Chrome and Safari fold it into a more generic Failed to fetch or Load failed. The CORS Error Code Breakdown covers the underlying status-to-cause mapping in more depth; this page is about the literal strings each browser prints.
Reference Table: Error String → Spec Rule → Fix
Each row below is keyed on the reason phrase (the part that varies with the actual fault), not the surrounding sentence. Header names are exact.
| Reason phrase (any browser) | Failing spec step | Root cause | Fix |
|---|---|---|---|
No 'Access-Control-Allow-Origin' header is present / CORS header 'Access-Control-Allow-Origin' missing |
CORS check, §4.9 | Response carried no Access-Control-Allow-Origin at all |
Emit the header on the actual response; see resolving No ‘Access-Control-Allow-Origin’ in Chrome |
The 'Access-Control-Allow-Origin' header has a value '...' that is not equal to the supplied origin |
CORS check, §4.9 | Header present but does not string-match the request Origin |
Reflect the exact origin from an allowlist per dynamic origin validation patterns |
The value of the 'Access-Control-Allow-Origin' header ... must not be the wildcard '*' when the request's credentials mode is 'include' |
CORS check + credentials rule, §3.2 | Wildcard returned for a credentialed request | Reflect exact origin and set Access-Control-Allow-Credentials: true; never use * with credentials |
Method ... is not allowed by Access-Control-Allow-Methods |
Preflight fetch, §4.8 | OPTIONS response omitted the real request method |
Add the method to Access-Control-Allow-Methods on the OPTIONS response |
Request header field ... is not allowed by Access-Control-Allow-Headers |
Preflight fetch, §4.8 | A custom request header was not declared as allowed | Add the header to Access-Control-Allow-Headers; see Access-Control header directives |
Response to preflight request doesn't pass access control check (Chrome) / Preflight response is not successful (Safari) |
Preflight fetch, §4.8 | OPTIONS returned a non-2xx status or lacked CORS headers |
Return 204/200 with full Access-Control-* set; see OPTIONS endpoint design |
CORS request did not succeed (Firefox) / Failed to fetch (Chrome) / Load failed (Safari) |
No usable response — fetch failed before the CORS check | TLS error, mixed content, connection refused, or no OPTIONS answer |
Fix transport first; see fixing “CORS request did not succeed” in Firefox |
Credentials flag is 'true', but the 'Access-Control-Allow-Credentials' header is '' |
CORS check, §3.2.5 | Credentialed request, but the header was absent or not literal true |
Set Access-Control-Allow-Credentials: true exactly |
... header contains multiple values '..., ...', but only one is allowed |
CORS check, §4.9 | Two layers both added Access-Control-Allow-Origin |
De-duplicate at the proxy; see header deduplication techniques |
Browser-by-Browser Wording Differences
The same three faults print differently in each engine. The table below shows the verbatim leading sentence so you can identify the engine from a pasted log.
| Fault | Chrome (Blink) | Firefox (Gecko) | Safari (WebKit) |
|---|---|---|---|
Missing Access-Control-Allow-Origin |
Access to fetch at '...' from origin '...' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. |
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at <URL>. (Reason: CORS header 'Access-Control-Allow-Origin' missing). |
Origin ... is not allowed by Access-Control-Allow-Origin. Status code: 200 |
| No usable response | Failed to fetch (thrown to JS) |
Cross-Origin Request Blocked: ... (Reason: CORS request did not succeed). Status code: (null). |
Load failed |
| Preflight rejected | Response to preflight request doesn't pass access control check: ... |
Cross-Origin Request Blocked: ... (Reason: CORS preflight response did not succeed). |
Preflight response is not successful. Status code: ... |
Two practical consequences follow. First, Firefox’s Status code: (null) is a strong signal that no HTTP response reached the browser — it is not a header problem. Second, Chrome throws Failed to fetch to JavaScript with no console detail for transport failures, so when a fetch() rejects with TypeError: Failed to fetch and the console is otherwise silent, treat it like Firefox’s CORS request did not succeed and check the transport, not the headers.
Mapping an Error String to Its Root-Cause Layer
Every CORS console string resolves to one of three layers. The diagram routes a pasted error to the layer that owns the fix.
Figure — one of three layers owns every CORS console error; the reason phrase tells you which.
Step-by-Step: Decode Any CORS Error
Step 1 — Capture the exact string, including the reason clause
Copy the whole console line, not a paraphrase. In Firefox the decisive token is inside the parentheses (Reason: ...); in Chrome it is the clause after blocked by CORS policy:. Preserve Status code: (null) if present — it is diagnostic.
Step 2 — Classify the layer with one question
Ask: did a real HTTP response arrive? If the message says Status code: (null), Failed to fetch, or Load failed with no reason clause, no usable response arrived — the fault is transport, and no header change will help. Otherwise a response arrived and the reason clause names the failing check.
Step 3 — Reproduce with curl to see the unfiltered response
curl is not bound by the same-origin policy, so it shows exactly what the server (and any proxy) returns:
curl -sS -D - -o /dev/null \
-H "Origin: https://app.example.com" \
https://api.example.com/v1/data
For a suspected preflight failure, send the OPTIONS request the browser would send:
curl -sS -D - -o /dev/null -X OPTIONS \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization" \
https://api.example.com/v1/data
Step 4 — Compare curl output to the browser verdict
If curl shows the correct Access-Control-Allow-Origin but the browser reports it missing, a layer between the browser and origin is stripping or duplicating it — inspect each hop. If curl also lacks the header, the origin never emitted it and the fix is server-side.
Step 5 — Apply the row’s fix and re-verify
Use the reference table row that matches your reason phrase. After the change, re-run the curl command and reload with DevTools open; the console line must disappear entirely, not merely change wording.
Edge Cases That Disguise the Real Cause
Opaque redirects. A 3xx redirect on a CORS request produces a follow-up request whose failure is reported against the final URL, not the one your code called. The console origin may not match your fetch() argument. Check the Network panel for an intermediate 302.
null origin. Sandboxed iframes, data: URLs, and some file-protocol contexts send Origin: null. A server allowlist that does not include the literal string null will fail the CORS check even though the page “looks” same-site. Do not reflect null blindly — it grants access to every sandboxed context.
Cached preflight masking a fix. After you correct an OPTIONS response, the browser may still use a preflight cached under Access-Control-Max-Age. A stale cache makes a fixed server look broken. See cache duration tuning and Max-Age; to force a fresh preflight, restart the browser or reduce Access-Control-Max-Age.
Duplicate headers from a proxy. If both the origin and a reverse proxy add Access-Control-Allow-Origin, the browser sees https://a.example, https://a.example and rejects it as multi-valued — even though each layer is individually correct. This is a proxy-layer fault, covered under header deduplication techniques.
DevTools + curl Verification Checklist
Common Mistakes
| Issue | Technical impact | Mitigation |
|---|---|---|
| Reading the sentence, not the reason clause | Time wasted fixing headers when the real fault is transport | Always decode by the Reason: / post-colon phrase |
Treating CORS request did not succeed as a header problem |
Endless header edits on a server that never received the request | Recognise Status code: (null) as “no response”; fix TLS/mixed-content/OPTIONS first |
| Trusting the Network panel over the console for pass/fail | Believing a request succeeded because headers are visible on the wire | The console reflects the post-parse verdict; duplicate or stripped headers still fail |
Assuming a 200 status means CORS passed |
Blocked responses can carry 200 and a full body |
CORS is enforced after receipt; check Access-Control-Allow-Origin, not the status |
| Testing only in one browser | A Chrome-specific Failed to fetch hides the detailed Firefox reason |
Cross-check in Firefox, whose Reason: clause is the most explicit |
| Not accounting for a cached preflight | A corrected OPTIONS still appears to fail |
Lower Access-Control-Max-Age or restart the browser when validating a preflight fix |
FAQ
Why do Chrome and Firefox report completely different messages for the same CORS failure?
Each browser writes its own console text from the same underlying Fetch algorithm. Chrome phrases failures as “has been blocked by CORS policy” with a specific reason clause, Firefox as “Cross-Origin Request Blocked … (Reason: …)”, and Safari with terse “Origin … is not allowed by Access-Control-Allow-Origin” text. The wording differs but the spec step that failed is the same, so decode by the reason clause, not the sentence structure.
Does a CORS error mean my server returned an error status?
No. A CORS error is raised by the browser after it receives the response, based on missing or mismatched Access-Control-* headers. The server can return 200 OK with a full body and the browser will still block JavaScript from reading it. The only exception is Firefox’s “CORS request did not succeed”, which means no usable HTTP response arrived at all.
Can I read the blocked response body from JavaScript to see what went wrong?
No. When the CORS check fails the Fetch spec returns an opaque network error to JavaScript, so status, headers, and body are all unreadable from script. You must inspect the raw response in the DevTools Network panel or reproduce the request with curl, which is not subject to the same-origin policy.
Why does the Network panel show the response headers but the console still reports them missing?
The Network panel shows what arrived on the wire, which can include headers a proxy added on one hop and another stripped, or duplicate Access-Control-Allow-Origin values that the browser rejects as malformed. The console reflects the browser’s post-parse verdict. Trust the console for pass/fail and the Network panel plus curl to locate which layer mutated the header.
Related
Topics in This Section
Fixing "CORS request did not succeed" in Firefox
Firefox's 'CORS request did not succeed' with Status code (null) means no HTTP response arrived at all — a TLS, mixed-content, or connection fault, not a header mismatch. Here's the fix.
Resolving "No 'Access-Control-Allow-Origin'" in Chrome
Chrome's 'blocked by CORS policy: No Access-Control-Allow-Origin header is present' means the actual response lacked the header. Diagnose origin, proxy stripping, and preflight vs real response.