You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Diagnosability: fetch/XHR error fidelity and modern AbortSignal (#196) (#200)
Implements the fetch/XHR diagnosability work tracked in #196 — making
transport failures observable to the embedding app instead of opaque —
plus modern `AbortSignal` support.
Requires the UrlLib transport-error accessors merged in
BabylonJS/UrlLib#33 (this PR bumps the UrlLib pin to `e86ffb3`).
> **Note:** the unhandled-promise-rejection hop that was originally part
of this PR has been **split out into #204** for its own design
discussion (it's engine-conditional — V8/Apple JSC only — and needs an
`#ifdef`-free rewrite). Everything remaining here is uniform across all
JS engines and carries no engine/platform preprocessor guards.
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 plain `Error{"fetch: network request failed"}` (no
`cause`/`code`/`url`, stack snapshotted in a worker tick = zero user
frames); XHR exposed nothing beyond `status === 0`.
- `fetch` rejects with a **`TypeError`** (stable message `"fetch
failed"`), detail nested under **`error.cause = {code, detail, url,
status}`** (Node/undici shape, not top-level own-properties).
- `cause.code`/`cause.detail` come 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.)
- The JS call-site **stack is captured synchronously** inside `fetch()`
(via the global `Error` constructor, which materializes frames even on
Chakra) and reattached to the rejection.
- `XMLHttpRequest` gains additive read-only
**`errorCode`/`errorDetail`** accessors; its standard `error` event +
`status === 0` behavior is unchanged.
### Hop 4 — `init.signal` + modern AbortSignal
`fetch()` ignored `init.signal` entirely
(`arcana::cancellation::none()`), and the `AbortSignal` polyfill
predated the modern spec.
- `AbortSignal`: `reason` (read-only), `throwIfAborted()`, static
`AbortSignal.abort(reason?)`, read-only `aborted`, and a default
**`AbortError`** reason (an `Error` whose `name` is `"AbortError"`; no
`DOMException` polyfill exists). `AbortController.abort(reason?)`
forwards the reason.
- `fetch` honors `init.signal` via its JS interface: an already-aborted
signal rejects synchronously; an in-flight abort cancels the transport
(`UrlRequest::Abort()`) and rejects with the signal's `reason`.
---
### 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`+`cause` shape
(refused / missing-asset), XHR `errorCode`/`errorDetail`, the modern
`AbortSignal` API, and fetch `AbortError` (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>
---------
Co-authored-by: Branimir Karadžić (via Copilot) <223556219+Copilot@users.noreply.github.com>
Copy file name to clipboardExpand all lines: Polyfills/AbortController/Readme.md
+13-2Lines changed: 13 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,10 +1,21 @@
1
1
# AbortController
2
2
Implements parts of [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/) and [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal). Provides a way to trigger the abort signal. *Work In Progress*
Copy file name to clipboardExpand all lines: Polyfills/Fetch/Readme.md
+33Lines changed: 33 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -28,3 +28,36 @@ Like `XMLHttpRequest`, `fetch()` supports loading local resources:
28
28
* Only `GET` and `POST` methods are supported (a `UrlLib` limitation shared with `XMLHttpRequest`).
29
29
* Only string request bodies are supported.
30
30
* Consistent with the fetch spec, the promise rejects only on transport-level failures. A completed request with a non-`2xx` status (e.g. `404`) still resolves, with `response.ok === false`.
31
+
32
+
## Transport-failure rejections
33
+
On a transport-level failure (DNS failure, connection refused, TLS failure, missing local
34
+
asset, ...) the promise rejects with a **`TypeError`** whose `message` is the stable string
35
+
`"fetch failed"`. The message is intentionally constant so crash-report grouping stays intact;
36
+
the variable detail is carried on `error.cause` (the Node/undici shape), never spread across the
37
+
message:
38
+
39
+
```js
40
+
try {
41
+
awaitfetch("https://does-not-resolve.invalid/");
42
+
} catch (error) {
43
+
error.message; // "fetch failed" (stable)
44
+
error.cause.code; // stable token, e.g. "CURLE_COULDNT_RESOLVE_HOST" (where available)
45
+
error.cause.detail; // full normalized UrlLib string (where available)
46
+
error.cause.url; // the requested URL
47
+
error.cause.status; // 0 for a transport failure
48
+
}
49
+
```
50
+
51
+
`error.cause.code` / `error.cause.detail` come from `UrlLib`'s normalized transport-error
52
+
accessors and are present on the backends that populate them (Apple, Linux); on backends that do
53
+
not yet (Windows, Android) they are omitted while the standard observable shape (a `TypeError`
54
+
with the stable message, plus `cause.url` / `cause.status`) is preserved. This is a deliberate,
55
+
strictly-additive superset of the spec: spec-conformant code only sees a `TypeError`, exactly as
56
+
in a browser, while BN-aware diagnostic code can read `cause` to distinguish a DNS failure from a
57
+
refused connection or a missing local asset.
58
+
59
+
The rejection's `stack` is captured synchronously at the `fetch()` call site (before the request
60
+
is handed to a worker thread), so crash reports can attribute the failing call rather than an
61
+
empty scheduler tick. (Engines that only materialize `.stack` when an error is thrown may omit
0 commit comments