Package: @warp-drive/core 5.8.2 (still present on main:
warp-drive-packages/core/src/request/-private/fetch.ts)
Affects: DEBUG builds only (the heuristic compiles to () => false in production).
Summary
The Fetch handler stamps a date header onto successful responses that lack one. It picks
between two strategies:
if (!isError && !isMutationOp && response.status !== 204 && !response.headers.has('date')) {
if (IS_MAYBE_MIRAGE()) {
response.headers.set('date', new Date().toUTCString()); // mutates in place
} else {
const headers = new Headers(response.headers); // safe clone path
headers.set('date', new Date().toUTCString());
response = cloneResponse(response, { headers });
}
}
IS_MAYBE_MIRAGE() is:
IS_MAYBE_MIRAGE = () =>
Boolean(
typeof window !== 'undefined' &&
((window as { server?: { pretender: unknown } }).server?.pretender ||
window.fetch.toString().replace(/\s+/g, '') !== 'function fetch() { [native code] }'.replace(/\s+/g, ''))
);
The toString() check treats any non-native window.fetch as "probably Mirage". But the
response being processed is still a real fetch() Response whose Headers carry the
immutable guard, so the in-place headers.set('date', ...) throws:
TypeError: Failed to execute 'set' on 'Headers': Headers are immutable
and the request rejects even though the network call succeeded.
Real-world triggers (none of them Mirage)
- SSR shoebox/replay wrappers that temporarily patch
window.fetch during hydration
(how we hit it: vite-ember-ssr installs one until its cached entries are consumed)
- error/performance instrumentation that wraps fetch (Rollbar, Sentry, APM agents)
- browser extensions injecting main-world fetch wrappers
core-js URL polyfills bundled by third-party scripts, which force-replace fetch
Two properties make this painful to debug:
- It's flaky.
IS_MAYBE_MIRAGE() is evaluated per response, so it races the
wrapper's install/uninstall. In our app the same reload sometimes crashed and sometimes
didn't, depending on whether the shoebox drained before the API response landed.
- The error loses its identity.
cloneError() rebuilds rejections as plain
new Error(message), so callers see Error, not TypeError, and the console points at
the store call site rather than the Fetch handler.
Note the precondition !response.headers.has('date') is met by every cross-origin API
response unless the server explicitly sends Access-Control-Expose-Headers: Date — Date
is not a CORS-safelisted response header. So for a typical dev setup (app origin ≠ API
origin), any wrapped fetch turns every successful GET into a rejection.
Reproduction
// in any DEBUG build app:
const orig = window.fetch;
window.fetch = function fetch(...args) { return orig.apply(this, args); }; // transparent wrapper
// any successful non-mutation store.request() whose response has no readable
// `date` header now rejects with:
// TypeError: Failed to execute 'set' on 'Headers': Headers are immutable
Suggested fix
Feature-detect instead of fingerprinting — attempt the in-place set and fall back to the
clone path when the headers are guarded:
if (!isError && !isMutationOp && response.status !== 204 && !response.headers.has('date')) {
try {
// Mirage/Pretender-style fake Responses have mutable headers
response.headers.set('date', new Date().toUTCString());
} catch {
// real fetch Response: Headers guard is "immutable"
const headers = new Headers(response.headers);
headers.set('date', new Date().toUTCString());
response = cloneResponse(response, { headers });
}
}
This removes the IS_MAYBE_MIRAGE fingerprint entirely for this branch (the streaming
check below it could keep it, or use a capability check as well). Happy to send a PR if
you'd take this shape.
Package:
@warp-drive/core5.8.2 (still present onmain:warp-drive-packages/core/src/request/-private/fetch.ts)Affects: DEBUG builds only (the heuristic compiles to
() => falsein production).Summary
The
Fetchhandler stamps adateheader onto successful responses that lack one. It picksbetween two strategies:
IS_MAYBE_MIRAGE()is:The
toString()check treats any non-nativewindow.fetchas "probably Mirage". But theresponse being processed is still a real
fetch()Response whoseHeaderscarry theimmutableguard, so the in-placeheaders.set('date', ...)throws:and the request rejects even though the network call succeeded.
Real-world triggers (none of them Mirage)
window.fetchduring hydration(how we hit it:
vite-ember-ssrinstalls one until its cached entries are consumed)core-jsURL polyfills bundled by third-party scripts, which force-replacefetchTwo properties make this painful to debug:
IS_MAYBE_MIRAGE()is evaluated per response, so it races thewrapper's install/uninstall. In our app the same reload sometimes crashed and sometimes
didn't, depending on whether the shoebox drained before the API response landed.
cloneError()rebuilds rejections as plainnew Error(message), so callers seeError, notTypeError, and the console points atthe store call site rather than the Fetch handler.
Note the precondition
!response.headers.has('date')is met by every cross-origin APIresponse unless the server explicitly sends
Access-Control-Expose-Headers: Date—Dateis not a CORS-safelisted response header. So for a typical dev setup (app origin ≠ API
origin), any wrapped fetch turns every successful GET into a rejection.
Reproduction
Suggested fix
Feature-detect instead of fingerprinting — attempt the in-place set and fall back to the
clone path when the headers are guarded:
This removes the
IS_MAYBE_MIRAGEfingerprint entirely for this branch (the streamingcheck below it could keep it, or use a capability check as well). Happy to send a PR if
you'd take this shape.