feat(shim): the web APIs pages die on, and abort for fetch - #31
Closed
pathscale wants to merge 4 commits into
Closed
feat(shim): the web APIs pages die on, and abort for fetch#31pathscale wants to merge 4 commits into
pathscale wants to merge 4 commits into
Conversation
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.
Owner
Author
|
Superseded by #33, which consolidates all chuzz work into one PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The web APIs the 104-site corpus found pages dying on, added to
WEB_API_SHIMin
apps/chuzz/src/document_loader.rs.Based on
feat/network-apis, notmaster—fetchandXMLHttpRequestlive there, and the second commit here extends them. Review or land that first.
Real, with nothing invented
escape/unescapeTextEncoder/TextDecoderAbortController/AbortSignalabort,timeoutandanyincludedString.prototype.substrsubstris not on the corpus list and cannot be. That report counts names apage looked up and did not find; a missing method on an existing prototype
raises
TypeError: not a callable functioninstead, an error class countednowhere. It surfaced from writing
unescapein terms of it and watching thatthrow. 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, withoutfetching. Most constructed
Images are preloaders that need only thecallback. 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 theIntersectionObserverbeside 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 agrid that divides by that width computes zero columns and renders nothing.
Path2D(1 site) really accumulates its path; what is missing is a canvascontext to read it.
ShadowRoot(1 site) is declared soinstanceofis answerable andnothing 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 isworse 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 incrementaldelivery, and a stub can only hand over everything at once or nothing.
the_lying_stubs_are_left_outfails if either appears, so a future change thatadds them has to be one that backs them with real data.
The second commit: abort reaches the network
fetchreadsinit.signal— an already-aborted one rejects without touchingthe network, and one that aborts later settles the promise with the signal's
reason.
XMLHttpRequest.abortwas 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_answerasserts exactly thatboundary — 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::testsevaluates it the way a page does and reads the answers back — 7 tests over the
new globals, plus 2 in
net_bridgefor abort.cargo test --workspaceisgreen, as are
fmtandclippy -D warnings.The pre-existing
embedded_ui_bundle_builds_the_solid_shell_in_boastillpasses, which matters here:
WEB_API_SHIMis evaluated into the browser chromeas 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, andscrollX/scrollYat 0 arethe 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 andasserted by a test.
atob/btoais the one addition nothing asked for. Re-capturing showed a pagefall from 215 nodes to 28, which reads as a regression and is not one:
String.prototype.substrlet its bundle run past the firstTypeError: not a callable function, far enough to clear the server-renderedmarkup 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.shagainst the previous run. Site ids only; theid-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 newlyreached ones now that scripts run further:
HeadersandFile.TypeError: not a callable functionfell on 3 sites (4→0, 4→2, 6→4) and rose onone (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
getComputedStyleand the DOM interface constructors, both of which need realengine data and are deliberately not faked here.