Debugging network problems
Read the Network panel properly, decode a CORS failure, capture a HAR file, and tell a timeout from an abort from a network failure.
The Network panel
| Tool | What it answers |
|---|---|
Filter box, Fetch/XHR | which requests were made by script, without images and fonts |
status:0 or method:POST filters | find the failures or the writes quickly |
| Request headers view | was the header actually sent, with the value you intended |
| Timing tab | was the time spent queued, connecting, waiting for the server, or downloading |
| Response tab on a failing call | the server's error body, which the console never shows |
| Preserve log | keep the requests from before a navigation or a reload |
| Throttling presets | reproduce a slow connection and expose races you cannot see locally |
- A request that never appears in the panel never left the browser: it was blocked, deduplicated by the cache, or the code threw before calling fetch.
- The queueing row in the timing tab is the browser's own connection limit. If most of the bar is queueing, your problem is concurrency, not the server.
- Searching all requests for a response header or a body string finds the call you forgot you made, which is often the actual bug.
- Copy as fetch from the context menu reproduces the request exactly, including headers, and is far faster than rebuilding it by hand.
Reading a CORS failure correctly
The console message names the cause once you read it as three parts: the request, the response headers, and the missing header. Everything else in the message is boilerplate.
Access to fetch at 'https://api.example.com/me' from origin
'https://app.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the
requested resource.
read as:
request origin ... https://app.example.com
resource ......... https://api.example.com/me
missing ......... Access-Control-Allow-Origin
variants and what they mean
"Response to preflight request doesn't pass access control check"
-> the OPTIONS request failed; check Allow-Methods and Allow-Headers
"The value of the 'Access-Control-Allow-Origin' header ... must not be the wildcard '*'
when the request's credentials mode is 'include'"
-> echo the exact origin instead of *# reproduce without CORS: curl is not a browser and ignores the policy
curl -i -X OPTIONS 'https://api.example.com/me' \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: PUT' \
-H 'Access-Control-Request-Headers: content-type'
# if the headers are missing here, the problem is on the server, not in your code
curl -i 'https://api.example.com/me' -H 'Origin: https://app.example.com'⚠️
A CORS error is not evidence that the request failed. For a simple POST the server already processed it, and the browser only withheld the response. Check the server logs before retrying, or you may create the record twice.
HAR files, and naming the failure
| Symptom | What it is | What to do |
|---|---|---|
AbortError | your code or a component aborted the signal | expected if you aborted on purpose; log the reason otherwise |
TypeError: Failed to fetch | the request never completed: offline, DNS, TLS or a blocked request | check the panel and the network, not the server |
TimeoutError | AbortSignal.timeout expired | consider a longer budget or a server-side change, and retry with backoff |
| Status 0 | no HTTP status was received | treat as no response, never as a server error |
| Status 504 or 502 | a proxy could not reach the origin | look at the gateway log, it knows which upstream failed |
| Status 200 with the wrong content type | a proxy or a login page answered instead of the API | verify the URL and the authentication state |
// name the failure at the source, so a report says which case it was
async function labelledFetch(url, init) {
try {
const res = await fetch(url, init);
if (!res.ok) throw new Error('HTTP ' + res.status + ' from ' + url);
return res;
} catch (err) {
if (err.name === 'AbortError') throw new Error('aborted: ' + url);
if (err.name === 'TimeoutError') throw new Error('timeout: ' + url);
if (err.name === 'TypeError') throw new Error('network or CORS: ' + url);
throw err;
}
}
// a HAR export captures every request, response and timing in one file
// DevTools -> Network -> right-click -> Save all as HAR with content
// it is the artifact to attach to a bug report, and it replays in other toolsA HAR file contains cookies and authorisation headers, so it is a credential-bearing artifact. Redact it before it leaves the team, and never attach one to a public issue.
FAQ
The request works in curl but fails in the browser. Why?
curl does not enforce the same-origin policy and does not send an
Origin header, so it never triggers CORS. If the browser call fails and curl succeeds, the server is missing the CORS response headers. It is not a client bug.How do I tell a timeout from an abort?
Both surface as an abort, but the error names differ:
AbortSignal.timeout produces a TimeoutError, while controller.abort() produces an AbortError. Check the name in the catch block and label the error before it is reported.Related
Same-origin policy, CORS and credentials Streaming and long-lived connections
Last refreshed 2026-09-18.