Diagnosability: fetch/XHR error fidelity and modern AbortSignal (#196)#200
Conversation
Previously every fetch()/XMLHttpRequest transport failure (DNS failure,
connection refused, TLS failure, missing local asset, ...) was flattened to a
single opaque signal -- fetch rejected with `std::runtime_error{"fetch: network
request failed"}` (a plain Error with no cause/code/url and a stack snapshotted
in a worker-thread scheduler tick, i.e. zero user frames), and XHR exposed
nothing beyond status 0. Field crash reports could not tell the failure classes
apart. This is hop 2 of BabylonJS#196, now unblocked by the
UrlLib transport-error accessors (BabylonJS/UrlLib#33).
fetch:
- Reject with a TypeError whose message is the stable string "fetch failed"
(kept constant so crash-report grouping stays intact), carrying the variable
detail on `error.cause = { code, detail, url, status }` -- the Node/undici
shape -- rather than as non-standard top-level own-properties.
- `cause.code`/`cause.detail` come from UrlLib's ErrorSymbol()/ErrorString();
they are present on backends that populate them (Apple, Linux) and omitted on
those that do not yet (Windows, Android), while the standard observable shape
(TypeError + stable message + cause.url/status) is preserved everywhere. This
is a strict, additive superset of the spec: spec-conformant code sees only a
TypeError, as in a browser; BN-aware code can distinguish failure classes.
- Capture the JS call-site stack synchronously inside fetch(), before SendAsync()
hands off to a worker thread, and reattach it to the rejection so crash reports
attribute the failing call. Uses the global JS Error constructor so engines
that materialize .stack from that path (incl. Chakra) capture live frames.
XMLHttpRequest:
- Add non-standard, additive read-only `errorCode`/`errorDetail` accessors that
expose the same UrlLib detail; the standard error event + status===0 behavior
is unchanged.
Also bump the UrlLib pin to e86ffb3 (BabylonJS#33) so the accessors are available, add
JS tests for the rejection shape and XHR diagnostics, and document both in the
polyfill Readmes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR improves diagnosability of transport-level failures in the fetch() and XMLHttpRequest polyfills by preserving structured error detail (from UrlLib) and capturing a synchronous call-site stack before work hops to a worker thread.
Changes:
fetch()now rejects transport failures asTypeError("fetch failed")with a structurederror.causeobject and an optionally reattached call-site stack.XMLHttpRequestgains additiveerrorCode/errorDetailaccessors for transport-failure diagnostics.- UrlLib pin is bumped to include transport-error accessor support; unit tests and READMEs are updated accordingly.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| Tests/UnitTests/Scripts/tests.ts | Adds unit tests asserting the new fetch TypeError + cause shape and XHR errorCode/errorDetail behavior. |
| Polyfills/XMLHttpRequest/Source/XMLHttpRequest.h | Declares new errorCode / errorDetail accessors. |
| Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp | Implements the new diagnostics accessors and wires them as instance properties. |
| Polyfills/XMLHttpRequest/Readme.md | Documents the new XHR transport-error diagnostics. |
| Polyfills/Fetch/Source/Fetch.cpp | Implements fetch failed TypeError rejections with cause and synchronous stack capture/reattachment. |
| Polyfills/Fetch/Readme.md | Documents the new fetch transport-failure rejection shape and stack behavior. |
| CMakeLists.txt | Updates UrlLib dependency pin to the commit providing transport-error accessors. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…or event - CaptureCallSiteStack: detect the global Error constructor via IsUndefined()/IsNull() instead of IsFunction(), matching the repo pattern (some JavaScriptCore/JSI builds report constructors as typeof 'object', which would silently disable synchronous stack capture). - XMLHttpRequest Readme: add the 'error' event to the supported-events list, since the implementation and tests rely on it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bghgary
left a comment
There was a problem hiding this comment.
[Reviewed by Copilot on behalf of @bghgary]
Comments inline. Main asks: implement JSC in this PR (it has the API, CI covers it); drop the opt-in flag — always track, routing to the existing UnhandledExceptionHandler; and make the two test-gating mechanisms consistent (compile-time #if defined, not a runtime engine-name string compare). Plus two minor V8-tracker robustness notes.
|
@bghgary Thanks for the thorough review — all of it is addressed in 97e2c68 (hop 3 refactor):
Validated locally on Win32: V8 Release — both |
fetch() previously ignored init.signal entirely (it passed arcana::cancellation::none() to both continuations), so AbortController could never cancel a request or reject with AbortError, and the AbortSignal polyfill predated the modern spec. This is hop 4 of BabylonJS#196. AbortSignal: - Add `reason` (read-only) and `throwIfAborted()`. - Add static `AbortSignal.abort(reason?)`. - Make `aborted` read-only (remove the setter). - Abort(reason) now records the reason, defaulting to an AbortError (an Error whose name is "AbortError", as there is no DOMException polyfill). - AbortController.abort(reason) forwards the reason to the signal. fetch: - Honor init.signal via its JS interface (so fetch stays decoupled from the AbortController C++ types): an already-aborted signal rejects synchronously with the signal's reason; otherwise an "abort" listener cancels the transport (UrlRequest::Abort()) and the completion continuation rejects with the signal's reason (an AbortError) instead of a network-error TypeError. The listener is torn down when the request settles, breaking the ownership cycle. Transport cancellation is effective on backends where UrlRequest::Abort() is implemented (Windows today; the curl/NSURLSession backends are a UrlLib follow-up noted in BabylonJS#196). AbortSignal.timeout() is left as a follow-up. Adds JS tests for the modern AbortSignal API and for fetch rejecting with an AbortError both pre-aborted and in-flight (validated on Win32, where the UrlLib backend honors Abort()). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
b2553a2 to
fa8c1f0
Compare
bghgary
left a comment
There was a problem hiding this comment.
[Reviewed by Copilot on behalf of @bghgary]
The fetch/XHR error-fidelity and AbortSignal changes look good. My one concern is the unhandled-promise-rejection tracking (inline) — could you split that into its own PR? I'll approve the rest as-is.
Implements the fetch/XHR diagnosability work tracked in #196 — making transport failures observable to the embedding app instead of opaque — plus modern
AbortSignalsupport.Requires the UrlLib transport-error accessors merged in BabylonJS/UrlLib#33 (this PR bumps the UrlLib pin to
e86ffb3).Each hop is a self-contained commit.
Hop 2 — fetch/XHR transport-error fidelity
Previously every transport failure (DNS failure, connection refused, TLS rejection, missing
app:///asset) was flattened:fetch()rejected with a plainError{"fetch: network request failed"}(nocause/code/url, stack snapshotted in a worker tick = zero user frames); XHR exposed nothing beyondstatus === 0.fetchrejects with aTypeError(stable message"fetch failed"), detail nested undererror.cause = {code, detail, url, status}(Node/undici shape, not top-level own-properties).cause.code/cause.detailcome from UrlLib; present on Apple/Linux, omitted on Windows/Android while the standard shape (TypeError+ stable message +cause.url/status) is preserved everywhere — a strict, additive superset of the spec. (This variance is a UrlLib HTTP-backend difference, not a JS-engine one.)fetch()(via the globalErrorconstructor, which materializes frames even on Chakra) and reattached to the rejection.XMLHttpRequestgains additive read-onlyerrorCode/errorDetailaccessors; its standarderrorevent +status === 0behavior is unchanged.Hop 4 —
init.signal+ modern AbortSignalfetch()ignoredinit.signalentirely (arcana::cancellation::none()), and theAbortSignalpolyfill predated the modern spec.AbortSignal:reason(read-only),throwIfAborted(), staticAbortSignal.abort(reason?), read-onlyaborted, and a defaultAbortErrorreason (anErrorwhosenameis"AbortError"; noDOMExceptionpolyfill exists).AbortController.abort(reason?)forwards the reason.fetchhonorsinit.signalvia its JS interface: an already-aborted signal rejects synchronously; an in-flight abort cancels the transport (UrlRequest::Abort()) and rejects with the signal'sreason.Validation
Built and ran the UnitTests suite on Win32 / Chakra and Win32 / V8; CI is green across the full matrix (Chakra/V8/JSC/JSI × Win32/UWP/Android/iOS/macOS/Linux, incl. sanitizers).
New tests (all engine-agnostic): fetch
TypeError+causeshape (refused / missing-asset), XHRerrorCode/errorDetail, the modernAbortSignalAPI, and fetchAbortError(pre-aborted + in-flight).Cross-repo follow-up
UrlRequest::Abort()only cancels the Windows backend today — making it interrupt the curl/NSURLSession transports is a separate UrlLib change (noted in #196).AbortSignal.timeout()is also a follow-up.Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com