Skip to content

feat(shim): the web APIs pages die on, and abort for fetch - #31

Closed
pathscale wants to merge 4 commits into
feat/network-apisfrom
feat/web-api-shims
Closed

feat(shim): the web APIs pages die on, and abort for fetch#31
pathscale wants to merge 4 commits into
feat/network-apisfrom
feat/web-api-shims

Conversation

@pathscale

@pathscale pathscale commented Aug 31, 2026

Copy link
Copy Markdown
Owner

The web APIs the 104-site corpus found pages dying on, added to WEB_API_SHIM
in apps/chuzz/src/document_loader.rs.

Based on feat/network-apis, not masterfetch and XMLHttpRequest
live there, and the second commit here extends them. Review or land that first.

Real, with nothing invented

API sites why it is honest
escape / unescape 1 pure string transforms with an Annex B specification
TextEncoder / TextDecoder 2 real UTF-8 both ways: surrogate pairs joined, unpaired surrogates as U+FFFD, overlong sequences rejected
AbortController / AbortSignal 2 the whole of it is a flag and a listener list; abort, timeout and any included
String.prototype.substr see below

substr is not on the corpus list and cannot be. That report counts names a
page looked up and did not find; a missing method on an existing prototype
raises TypeError: not a callable function instead, an error class counted
nowhere. It surfaced from writing unescape in terms of it and watching that
throw. It is defined rather than assigned, so it does not become enumerable on
every string in the page.

Stubs, each labelled as one in the file

  • Image (4 sites) reports every image as loaded, asynchronously, without
    fetching. Most constructed Images are preloaders that need only the
    callback. Code that waits for the load and then reads pixels or natural
    dimensions gets nothing, and the zero dimensions are left honest rather than
    invented for exactly that reason. Images the document references are still
    fetched and painted by the engine; this is only the JavaScript constructor.
  • ResizeObserver (1 site) never fires, unlike the IntersectionObserver
    beside it. The difference is what an invented entry would have to say:
    visibility has an answer that is right for most of a page, and a size does
    not. The only entry this could deliver carries a zero contentRect, and a
    grid that divides by that width computes zero columns and renders nothing.
  • Path2D (1 site) really accumulates its path; what is missing is a canvas
    context to read it.
  • ShadowRoot (1 site) is declared so instanceof is answerable and
    nothing is an instance of it, which is the truthful answer for an engine that
    builds no shadow trees.

Deliberately still absent

  • getComputedStyle (3 sites). A stub answering '' for every property is
    worse than the ReferenceError it replaces. Today the script throws and stops,
    which is visible; with a lying stub it continues, measures nothing, and lays
    the page out wrongly, which reads as an engine bug. Backing it with real style
    data is engine work.
  • ReadableStream (2 sites). A page reaching for it wants incremental
    delivery, and a stub can only hand over everything at once or nothing.

the_lying_stubs_are_left_out fails if either appears, so a future change that
adds them has to be one that backs them with real data.

The second commit: abort reaches the network

fetch reads init.signal — an already-aborted one rejects without touching
the network, and one that aborts later settles the promise with the signal's
reason. XMLHttpRequest.abort was an empty function and now does the same.

Half of this is honest and the other half is named as what it is not: the
request keeps running, because the host has already spawned it and there is no
cancellation channel back. What it buys is the observable half, which is the
half pages depend on. aborting_in_flight_drops_the_answer asserts exactly that
boundary — the server is contacted and does reply, and the page must not see
the reply.

Tests

The shim is a JavaScript string in a Rust file that nothing else in the build
parses, so a syntax error in it is not a compile error: it is a page that
renders as if the shim were absent, on every site. document_loader::tests
evaluates it the way a page does and reads the answers back — 7 tests over the
new globals, plus 2 in net_bridge for abort. cargo test --workspace is
green, as are fmt and clippy -D warnings.

The pre-existing embedded_ui_bundle_builds_the_solid_shell_in_boa still
passes, which matters here: WEB_API_SHIM is evaluated into the browser chrome
as well as into pages, so an addition that broke Solid would break the browser
itself.

Two more commits, and what measuring them found

DOMException, top/parent/self/frames, and scrollX/scrollY at 0 are
the tail of the corpus list past the ranked table; the omissions there
(NodeList, DocumentFragment, CharacterData, Intl, ActiveXObject,
WebAssembly, define, require) are recorded in the file with the reason and
asserted by a test.

atob/btoa is the one addition nothing asked for. Re-capturing showed a page
fall from 215 nodes to 28, which reads as a regression and is not one:
String.prototype.substr let its bundle run past the first
TypeError: not a callable function, far enough to clear the server-rendered
markup and rebuild it, and then it hit atob. Four of the twelve did the same.
A missing global is only counted once something reaches it, so fixing one defect
is what surfaces the next.

Verification

The twelve sites whose logs named a global this branch supplies, re-captured
with scripts/render-check.sh against the previous run. Site ids only; the
id-to-host map is private and the logs are not committed.

Every global this branch adds is gone from all twelve logs. What is left is
exactly the deliberate omissions (getComputedStyle, ReadableStream,
NodeList, WebAssembly), two names the pages define themselves, and two newly
reached ones now that scripts run further: Headers and File.

TypeError: not a callable function fell on 3 sites (4→0, 4→2, 6→4) and rose on
one (3→5) where the bundle now runs past its first failure into new ground.

Geometry barely moves, and that is the honest result. Nodes and boxes are
identical on 7 of 12. Three gained: +34 nodes/+13 boxes, +50/+10, and one lost 2
nodes with no box change. Two are not comparable: one page served a WAF
challenge instead of itself this run (215 nodes → 28 is that, not a render
regression), and the twelfth's counts are unchanged.

So: the error class disappears, and the pages do not visibly change much
yet.
That is expected here. These shims stop scripts dying; the thing standing
between a running script and a rendered page on most of this corpus is
getComputedStyle and the DOM interface constructors, both of which need real
engine data and are deliberately not faked here.

meh added 4 commits August 31, 2026 20:27
Ranked by sites affected over a 104-site corpus, after the runtime-script-fetch
fix stopped scripts being dropped and let more of them run far enough to reach
these: `Image` (4 sites), `TextEncoder` (2), `AbortController` (2), and
`ResizeObserver`, `Path2D`, `ShadowRoot` and `unescape` (1 each).

Real implementations, with nothing invented:

- `escape` / `unescape`, the Annex B pair. Pure string transforms with a
  specification, so there is nothing to fake.
- `TextEncoder` / `TextDecoder`, real UTF-8 both ways, including surrogate
  pairs, unpaired surrogates as U+FFFD, and overlong sequences rejected. The
  callers that reach for these are hashing or framing bytes, where an encoder
  that got the multi-byte cases wrong would hand back a plausible array of the
  wrong length and fail somewhere else entirely, as a bad digest.
- `AbortController` / `AbortSignal`, including `abort`, `timeout` and `any`.
  The whole of it is bookkeeping over a flag and a listener list, with no engine
  support to wait for.
- `String.prototype.substr`. Also Annex B, also absent, and this one is not on
  the corpus list and cannot be: the report counts names a page looked up and
  did not find, and a missing method on an existing prototype raises
  `TypeError: not a callable function` instead, an error class counted nowhere.
  It surfaced from writing `unescape` in terms of it and watching that throw.

Stubs, each labelled as one in the file:

- `Image` reports every image as loaded, asynchronously, without fetching. Most
  constructed `Image`s are preloaders that only need the callback. Code that
  waits for the load and then reads pixels or natural dimensions gets nothing,
  and the zero dimensions are left honest rather than invented for that reason.
  Images the document references are still fetched and painted by the engine.
- `ResizeObserver` never fires, unlike the `IntersectionObserver` above it. The
  difference is what an invented entry would have to say: visibility has an
  answer that is right for most of a page, and a size does not. The only entry
  this could deliver carries a zero `contentRect`, and a grid that divides by
  that width computes zero columns and renders nothing.
- `Path2D` really accumulates its path; what is missing is a canvas context to
  read it.
- `ShadowRoot` is declared so `instanceof` is answerable and nothing is an
  instance of it, which is the truthful answer for an engine with no shadow
  trees.

Deliberately still absent, with the reasoning in the file and a test that fails
if either appears without real data behind it:

- `getComputedStyle` (3 sites). A stub answering '' for every property is worse
  than the ReferenceError it replaces: today the script throws and stops, which
  is visible, and with a lying stub it continues, measures nothing and lays the
  page out wrongly, which reads as an engine bug.
- `ReadableStream` (2 sites). A page reaching for it wants incremental
  delivery, and a stub can only hand over everything at once or nothing.

The shim is a JavaScript string in a Rust file that nothing else in the build
parses, so a syntax error in it is not a compile error: it is a page that
renders as if the shim were absent, on every site. The tests evaluate it the way
a page does and read the answers back.
`AbortController` is real now, so the consumer side can use it. `fetch` reads
`init.signal`: an already-aborted one rejects without touching the network, and
one that aborts later settles the promise with the signal's reason.
`XMLHttpRequest.abort` was an empty function and now does the same, firing
`onabort` and returning `readyState` to 0.

Half of this is honest and the other half is named as what it is not. The
request itself keeps running: the host has already spawned it and there is no
cancellation channel back, so nothing here closes a socket. What it buys is the
observable half, which is the half pages depend on — the promise settles now,
and the handler does not run later against a component that has been torn down.
`aborting_in_flight_drops_the_answer` asserts exactly that boundary: the server
is contacted and does reply, and the page must not see the reply.

The tests now install the web-API shim before this one, in the order
`browser.rs` and `load_for_capture` both use, because `AbortController` comes
from there and this only honours a signal because it does.
…osition

The tail of the corpus's missing-globals list, past the table the handover
ranked. Three more are honest in JavaScript alone, and the rest are recorded in
the file as omissions with the reason, so the next reader does not add them from
the report.

Real:

- `DOMException`. A name, a message and a legacy code, and what pages actually
  do with one is read `error.name === 'AbortError'`. Adding it also gives the
  abort machinery the type a browser really throws, so `AbortController`'s
  default reason is no longer an `Error` wearing the right name.
- `top`, `parent`, `self`, `frames`, `frameElement`. There are no frames here,
  so a document is its own top. Frame-busting code compares `top !== self` and
  gets `false`, which is correct rather than convenient.
- `scrollX` / `scrollY` and their `pageXOffset` aliases, at 0. Honest at load,
  which is when the scripts that read them run, and the same choice
  `IntersectionObserver` above already makes: a lazy loader concludes it is at
  the top of the page and shows what is above the fold. A page that binds a
  scroll handler and recomputes from these will not see the view move; making
  them true is engine work.

Left out, with the reasoning in the file and a test that fails if any appears:

- `NodeList`, `DocumentFragment`, `CharacterData`, `KeyboardEvent`,
  `HTMLVideoElement`. `ShadowRoot` is declared precisely because nothing in this
  engine is one, so `instanceof` answering `false` is true. These are the
  opposite case: the document really does contain node lists and fragments, so
  an empty constructor would answer `false` about objects that genuinely are
  instances, and a branch meaning to take the DOM path would silently take the
  other one. They belong with the engine's DOM bindings, next to the prototypes
  they have to be related to.
- `Intl`. `String(value)` for `NumberFormat` and `DateTimeFormat` keeps a script
  alive at the cost of rendering unformatted numbers and raw date strings as
  though they were the page's own output, and the locale data behind a real one
  is not a shim.
- `ActiveXObject`, reported by one site. No browser has it, and a page reaching
  for it without a `typeof` guard throws in Chrome too. The report is not a
  defect of ours.
- `WebAssembly`, `define` and `require`, which are engine and module support.
… not

Real base64, both ways, and the one addition here nothing asked for in advance.

Re-capturing the twelve affected sites showed a page fall from 215 nodes to 28,
which reads as a regression and is not one: `String.prototype.substr` let its
bundle run past the first `TypeError: not a callable function`, far enough to
clear the server-rendered markup and rebuild it, and then it hit `atob`. Four of
the twelve did the same. A missing global is only counted once something
reaches it, so fixing one defect is what surfaces the next, and the low node
count was the measurement working rather than failing.

`atob` accepts whitespace anywhere and optional padding, which is what a page
decoding a header or a data URL relies on, and both throw an
`InvalidCharacterError` DOMException on input that is not theirs to decode.
@pathscale

Copy link
Copy Markdown
Owner Author

Superseded by #33, which consolidates all chuzz work into one PR.

@pathscale pathscale closed this Aug 31, 2026
@pathscale
pathscale deleted the feat/web-api-shims branch September 1, 2026 00:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant