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:

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:

  1. The CORS check (WHATWG Fetch §4.9). After a response is received, the browser compares the response’s Access-Control-Allow-Origin value against the request’s Origin. Absence, mismatch, or a wildcard used with credentials all fail this check.
  2. The CORS-preflight fetch (WHATWG Fetch §4.8). Before a non-simple request, the browser sends an OPTIONS request and validates the response’s Access-Control-Allow-Methods and Access-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.

Routing a CORS error string to its root-cause layer A decision flow. Starting from a console error string, the first question is whether any HTTP response arrived. If none arrived, the fault is in the transport layer. If a response arrived, the next question is whether the failure names a method or header, which points to the preflight layer, otherwise to the CORS check layer on the actual response. Pasted console error read the reason phrase Did any HTTP response arrive? Status code (null) / Failed to fetch = no No Transport layer TLS, mixed content, refused, no OPTIONS Yes Does it name a method or header? or say "preflight" Yes Preflight layer OPTIONS response: Allow-Methods/Headers No CORS check layer Actual response: Allow-Origin / credentials

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.

Topics in This Section