fix: normalize TypeError to AbortError when fetch is aborted during OPTION request#1549
Conversation
…ORS preflight
When an AbortSignal fires while a CORS OPTIONS preflight is in-flight,
browsers (notably Chrome) reset the TCP connection and reject the fetch()
promise with TypeError ("Failed to fetch") instead of AbortError. This
occurs because the preflight runs with its own inner fetch params, and
the abort signal may not propagate to those inner params before the
preflight returns a plain network error — which the Fetch spec maps to
TypeError rather than AbortError.
The result is that the abort-detection logic in the tile loading catch
chain (which checks both signal.aborted and error.name === 'AbortError')
can fail to recognize the cancel as clean, permanently marking the tile
as FAILED.
Fix this in the default fetchData implementation by catching TypeError
immediately after the fetch() call — the earliest microtask hop — and
re-throwing as AbortError when signal.aborted is true.
Ref: whatwg/fetch#443
|
Hello! Thanks for the report and the fix but this seems like something that should be addressed by browsers and whatwg/fetch#443 only outlines why potentially sensitive preflight server-response information (like error codes) should be hidden. Not client-side evaluated cancellations. Is there an issue discussing the behavior for abort specifically in this case? |
|
Thanks for reviewing this. I can dig in a bit more, but my understanding from this ticket is that getting the TypeError back from a failed preflight, in this case due to an abort, is expected behavior. |
|
From whatwg/fetch#443:
The issue explains why they can't return more complete server response information in the error but this should not include client-triggered cancellations. |
|
This comment points out that it’s a low level behavior whatwg/fetch#443 (comment). This can be reproduced using XMLHttpRequest. My understanding is that when you abort a typical request, like a GET the onabort fires, and that’s it. It is easy to tell what’s happened. With the OPTIONS request, when you abort, you get the onabort callback AND a very generic error that can’t be disambiguated. Fetch throws the error, as it should, but now if the error bubbles up before the onabort, calling libraries can misinterpret the cause of the error. The privacy discussions seem to indicate that at a browser level we shouldn’t expect any useful errors to avoid leaking information. I can see why arguably an abort should be handled differently (is the client generating the error), but it seems unlikely that we’ll get a big change like this addressed in Chrome. I’m not at my laptop, right now, but I’d like to play with some toy examples. |
fix: normalize TypeError to AbortError when fetch is aborted during CORS preflight
When we updated to the latest version of 3DTilesRendererJS we began noticing missing tiles on some tilesets. We tracked the issue down to how network abort interacts with OPTIONS preflight requests.
Problem
When navigating a large tileset, the LRU cache will eventually fill and begin evicting
tiles. Each tile has an associated
AbortController; when the LRU evicts a tile, theeviction callback fires
controller.abort()to cancel any in-flight download for thattile (L1553).
If that download is mid-CORS-OPTIONS-preflight when the abort fires, Chrome resets the
underlying TCP connection and
fetch()rejects withTypeError: Failed to fetchinsteadof
AbortError. This is a known spec-level race: the preflight runs with its own innerfetch params, and the abort signal may not propagate into those params before the
preflight returns a plain network error — which the Fetch spec maps to
TypeErrorratherthan
AbortError. See whatwg/fetch#443.The
catchhandler atL1770–1776
has two guards to detect a clean abort:
Both guards fail for the Chrome CORS preflight case:
signal.abortedcan befalseifthe
TypeErrorarrives before the abort has fully propagated, anderror.nameis'TypeError'rather than'AbortError'. The tile is permanently markedFAILEDandwill not be retried.
This bug only surfaces in practice when:
With the default 400 MB byte limit this can happen mid-session on large tilesets.
Fix
Normalize the error in the default
fetchDataimplementation. Iffetch()rejects withTypeErrorandsignal.abortedis alreadytrue, re-throw asAbortErrorso theexisting abort-detection logic in the catch chain handles it cleanly:
Catching at the
fetchDatalevel (one microtask after the rejection) is the earliestpoint where
signal.abortedcan be checked, before the error travels through thedownstream
.then()chain.References
that all CORS preflight failures collapse to
TypeErrorcontroller.abort()