CORS 'Did Not Succeed': Find the Real Failure
Curl the URL before touching headers — 'CORS request did not succeed' means the request never completed, so dead servers and blockers come first..
20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.
- ✓A browser with DevTools and a spare clean profile
- ✓A terminal where you can run curl -v
- ✓One cross-origin fetch call you can reproduce on demand
- It means the request never completed — don't add CORS headers until curl returns a real response
- Run curl -v first: refused means dead server, resolve errors mean DNS, cert errors mean TLS
- Retest in a clean profile: success there fingers an extension or blocker, not your code
- Read the Network tab: (failed) with no status is transport, an HTTP status means headers — check Status before trusting the console
Imagine mailing a letter that never arrives, and the post office stamps it 'delivery failed'. You'd check whether the address exists and the road is open — you wouldn't repaint the recipient's mailbox. 'CORS request did not succeed' is that stamp: the request never completed its trip, so repainting headers (the mailbox) can't help. Check the road first: is the server up, does the address resolve, did anything intercept the mail?
Your fetch throws, and the console says the CORS request did not succeed. Instinct screams CORS headers — you open the server config and start adding Access-Control-Allow-Origin everywhere. Thirty minutes later nothing changed, because headers were never the problem. No response ever arrived to carry them.
This message is Firefox's way of saying the request never completed. The server might be down. DNS might fail. The certificate might be expired. An extension might have killed the flight. The page might be https calling http. Every one of those dies before CORS headers could matter, yet the console still says the C-word and sends you to the wrong docs.
This guide teaches the split that ends the confusion: curl first to test connectivity without browser interference, a clean profile to rule out extensions, the Network tab to read failed versus blocked, and response.ok plus timeouts so your code reports the true cause instead of a shrug. Work the checks in order and you'll name the killer in minutes instead of burning a sprint on headers that were never missing.
Not a Header Problem: The Request Never Completed
Read the sentence literally: the CORS request did not succeed. Not 'was rejected', not 'lacked a header' — it never finished. A classic CORS header failure looks different: the browser completes an HTTP exchange, finds no Access-Control-Allow-Origin, and blocks your script from reading a response that fully arrived. You can see that response in the Network tab with a status code attached. With did-not-succeed there is no status, no bytes, no response to attach headers to. The flight crashed; arguing about landing permission misses the point.
This distinction decides your first hour. Header failures are fixed in server config — add the origin, answer the preflight, expose the needed headers. Incomplete requests are fixed in connectivity — start the dead server, repair DNS, renew the cert, disable the killer extension, unify the scheme. Every minute spent on the wrong list is wasted, and the console's wording pushes newcomers toward the wrong one because it contains the letters C-O-R-S. Train yourself to translate the message on sight: did-not-succeed equals transport, always.
Your code can enforce the split too. A fetch that rejects with TypeError never completed; a fetch that resolves with res.ok false completed with an error status. Log err.name and message in every catch, and check res.ok before parsing — the snippet with this section shows the pattern. Teams that classify at the call site get bug reports saying 'never completed, curl next' instead of 'CORS broken?', and their fixes land in the right layer the first time.
Server Down, DNS, and TLS: Failures That Look Like CORS
Three server-side deaths wear the CORS costume. A stopped or crashed API refuses connections, and Firefox reports each fetch as did-not-succeed because nothing ever answered. Confirm in seconds: curl -v the same URL and watch for an instant refusal, then check the server process and its listening log. If the backend moved ports in a deploy, diff the page's base URL against the new listen line — the frontend env var is the usual stale piece, and it announces itself in the first log line once you print it at boot.
DNS failures look identical from the page. A renamed host, an expired dev tunnel URL, or a VPN that hijacks resolution means the browser never found an IP to call. curl says 'Could not resolve host' plainly, while the console mumbles about CORS. Test the exact hostname the page uses — not localhost when the page calls a tunnel domain — and check whether the failure follows the network (office vs home vs hotspot) to finger VPNs and captive portals.
TLS failures are the sneakiest because everything looks configured. An expired cert, a hostname that doesn't match the cert, or a corporate proxy re-signing traffic with an untrusted root all kill the handshake before HTTP starts. curl -v spells it out ('certificate has expired', 'unable to get local issuer certificate'), and the fix is renewal or trust, never headers. Monitor cert expiry from outside your network — office caches and pinned roots can mask a public breakage for hours, as our incident below proves.
Extensions, Ad Blockers, and Corporate Proxies That Kill Requests
Privacy extensions, ad blockers, script managers, and antivirus proxies all sit between your page and the network with permission to terminate requests. When one matches your API host or a tracking-looking path, the fetch dies inside the browser and surfaces as the same did-not-succeed line a dead server would produce. Your code is innocent, the server is innocent, and only the middlebox knows. The tell is distribution: it fails on your machine but passes in CI, on teammates' laptops, and on phones — because the killer lives in your profile.
The clean-profile test rules this in or out in under a minute. Chromium guest windows and fresh Firefox profiles load zero extensions by default; if the request succeeds there, bisect your daily profile by halves until the single culprit shows. Common offenders include over-broad blocklists that match api/analytics paths, corporate proxies that re-sign TLS with an untrusted root, and VPN clients that route localhost oddly. Document the find for the team — the next person with the same symptom gets a one-line fix.
Service workers deserve a mention as the self-inflicted middlebox. A stale worker can serve cached failure responses or route requests to a dead precache, faking a network fault with your own code. During debugging, bypass it: DevTools → Application → Service Workers → Bypass for network, or unregister the worker entirely. If the failure vanishes, the network was fine all along and your caching strategy needs the fix, not your CORS config.
Mixed Content: http on an https Page Dies Silently
An https page that fetches http:// is asking the browser to downgrade a secure context, and modern browsers refuse. Chromium blocks it as mixed content before any packet leaves; Firefox reports the fetch as not succeeding. Either way no CORS exchange happens, so no header can excuse it. The trap springs during deploys: everything works on http localhost, then staging goes https and the same code starts failing with a message that screams CORS at you.
Diagnosis is one glance at the Network tab: the row reads (blocked:mixed-content) and the console names mixed content alongside the failure. The durable fix is architectural, not per-request: keep the page and the API on one scheme and one origin. In dev, proxy /api through Vite or webpack so the browser never makes a cross-scheme call. In production, serve the API behind the same https host. The mkcert tool gives you trusted local https when the page must stay secure during development.
Treat scheme as part of the URL contract and review it in every deploy diff. A base URL that flips from https to http (or gains a hardcoded :80) deserves the same scrutiny as a changed hostname. Log the full request URL in development on failure — scheme included — so the next mismatch confesses in the first line of output instead of hiding behind a CORS costume for an afternoon.
Reading the Network Panel: Failed vs Blocked vs CORS Error
The Network tab settles every argument about this error if you read three fields in order. First the Status column: (failed) or (blocked:*) with no HTTP code means the request never completed — work the connectivity list. A numeric status (200, 403, 500) with a CORS console line means the exchange finished and headers are genuinely at fault. This one column splits the two universes, yet most developers stare at the console and never look at it.
Second, the Timing tab. An instant failure — 1ms and dead — points at a client-side block, a refusal, or mixed content: something said no immediately. A long Waiting phase that ends in failure points at stalls: a slow endpoint, a buffering proxy, or a hung server that accepted the socket and never answered. Pair the timing with curl's verbose output, which timestamps DNS, TCP, and TLS phases separately and shows exactly which phase never finished.
Third, match the console text to the row. 'Did not succeed' alone beside a (failed) row confirms an incomplete request — don't open header docs. A CORS Allow-Origin line beside a 200 row confirms headers — now open them. Make this three-field read a team habit and the average 'CORS' ticket stops being a 2-hour mystery and becomes a 10-minute classification with the fix attached.
The Checklist That Stops 'CORS' Wild-Goose Chases
Run this order and stop at the first failure. One: curl -v the exact URL the page calls, from the same machine. Any HTTP response means connectivity works — now headers and page code earn attention. Refused, resolve errors, or cert errors name the true culprit in one line. Two: retest in a clean profile with no extensions. Success there means a middlebox, not a server fault — bisect the extensions. Three: read the Network row's Status, Timing, and console text together to confirm transport versus headers.
Four: compare schemes — an https page must not call http APIs, so proxy /api through the dev server for a single origin. Five: wrap the fetch with a 10-second AbortSignal timeout and a catch that logs err.name, so stalls and refusals stop looking identical. Six: only now, with a curl response in hand and a numeric status in the Network tab, open the CORS header docs and fix the actual header gap.
Record the classification with every ticket: curl result, profile result, Network status. Three lines turn the next 'CORS?' report into a 5-minute confirmation instead of a fresh investigation. The message stops being scary once your team reads it as what it is — a request that never finished, with a short list of usual killers.
The Expired Cert That Looked Like a CORS Bug for 2 Hours
- 'Did not succeed' is a connectivity verdict — curl from outside your network before anyone edits header config.
- Office DNS and proxies can mask public breakage; synthetic checks must run from outside the building.
- Cert expiry alerts at 30 and 7 days turn a 2-hour outage into a calendar task nobody notices.
| File | Command / Code | Purpose |
|---|---|---|
| did-it-connect.js | async function fetchJson(url) { | Not a Header Problem |
| reachability.js | async function probe(url) { | Server Down, DNS, and TLS |
| clean-room.js | async function reportEnvironment() { | Extensions, Ad Blockers, and Corporate Proxies That Kill Req |
| same-origin-api.js | async function getUsers() { | Mixed Content |
| read-the-row.js | async function timedFetch(url) { | Reading the Network Panel |
Key takeaways
Common mistakes to avoid
5 patternsAdding CORS headers to fix a request that never completed
Calling http:// from an https page and reading it as CORS
Testing only in your daily browser full of blockers
Reading only the console and ignoring the Network tab
Fetching without a timeout, then misreading the failure
Interview Questions on This Topic
How does 'CORS request did not succeed' differ from a normal CORS header error?
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.
That's Browser. Mark it forged?
6 min read · try the examples if you haven't