Why fetch() Returns Opaque Response — and What You Can Still Do

(Updated: July 16, 2026 ) fetch CORS JavaScript browser

You fetch a CDN URL, response.type is "opaque", status is 0, body is empty. Stack Overflow says “add mode: 'no-cors'.” That “fix” is how you got here. Opaque is not a bug — it is the browser hiding cross-origin data you were never allowed to read. The request may still leave the machine; JavaScript simply cannot inspect the result.

What no-cors actually does

const res = await fetch(url, { mode: "no-cors" });
console.log(res.type);   // "opaque"
console.log(res.status); // 0
console.log(res.ok);     // false (always for opaque)
// body / headers are unusable for app logic

In no-cors mode the browser restricts the request to a simple subset (think image/script-like requests). You cannot set arbitrary headers or send JSON POST bodies the way you would for an API. The response is intentionally sealed so a random website cannot read another origin’s data by “just fetching it.”

Legitimate uses are narrow:

  • Fire-and-forget pings where you do not need the body (rare; navigator.sendBeacon is often clearer)
  • Historical Cache API / Service Worker patterns that store opaque responses for offline shells
  • Embedding cross-origin media via tags (<img>, <script>) — which are not fetch at all

Not legitimate: “I want JSON from someone else’s API without CORS.”

Response types you will see

response.typeMeaning
basicSame-origin (or filtered same-origin rules)
corsCross-origin and CORS allowed you to read
opaqueCross-origin, sealed (no-cors)
opaqueredirectManual redirect mode hit an opaque redirect
errorNetwork error response object

People confuse opaque with “failed.” Failure often surfaces as a thrown TypeScript/JS error on network failure, or as a readable cors/basic response with a 4xx/5xx status. Opaque specifically means “hidden,” not “broken.”

CORS modes quick map

ModeRead body?Typical use
cors (default when you need data cross-origin)If ACAO allowsAPIs
same-originSame origin onlyStrict apps
no-corsNeverOpaque only

If you need headers or body, fix server Access-Control-Allow-Origin (and credentials rules). Frontend cannot invent permission. Removing mode: "no-cors" is usually step one — you want the browser to attempt a CORS read and show you the real error.

Service workers and caches

Opaque responses can be put into the Cache API. That surprises teams who later try to res.json() from cache and get nothing. If your offline strategy needs to read JSON, cache only responses your origin is allowed to read (cors or basic), or cache same-origin copies produced by your own proxy.

// Pseudo: only cache readable JSON
const res = await fetch("/api/bootstrap");
if (res.type === "basic" || res.type === "cors") {
  await cache.put(request, res.clone());
}

Proxies done safely

When you cannot change the upstream CORS policy:

Browser → your API → upstream

Upstream never talks to the browser. You control caching, API keys, and rate limits. Guardrails:

  • Allowlist upstream hosts; do not proxy arbitrary URLs (SSRF)
  • Strip cookie forwarding unless required
  • Cache carefully; do not amplify abusive traffic
  • Return clear errors when upstream is down — your clients should see JSON, not opaque mysteries

Debugging checklist

  1. Remove mode: 'no-cors' if you need data.
  2. Inspect the request in Network: did a preflight OPTIONS fire? Did the real response include ACAO?
  3. Confirm ACAO matches your exact origin (scheme, host, port).
  4. Remember error responses sometimes omit CORS headers → browser reports CORS instead of 401.
  5. Check whether a Service Worker rewrote the fetch and returned a cached opaque entry.
  6. Verify you are not reading a CDN URL that only allows media tags, not XHR/fetch.
  • CORP / COEP on modern cross-origin isolation can block reads even when classic CORS looks fine — different headers, same “I cannot see the body” feeling.
  • CORS for fonts and canvases has extra tainting rules; opaque canvas pixels refuse toDataURL.
  • Extensions and local tools that “bypass CORS” do not change what production users’ browsers enforce.

Bottom line

Opaque means “you chose a mode that cannot read,” or you are looking at a sealed cross-origin result. For APIs, use proper CORS or a controlled proxy. no-cors is not a skeleton key — it is a lock that keeps your JavaScript honest.