Repository navigation
Fetch: the iterator attributes, the disturbed-body lock, and WebIDL's record order - #3247
Merged
Merged
Conversation
…` WebIDL's attributes https://webidl.spec.whatwg.org/#es-iterator-prototype-object: "an iterator prototype object must have a `next` data property with attributes `{ [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true }`". Jint's three were `enumerable: false`, which is ECMA-262's rule for a built-in function property (https://tc39.es/ecma262/#sec-ecmascript-standard-built-in-objects) and what `[JsFunction]` defaults to. web-platform-tests' `fetch/api/headers/headers-basic.any.js` reads the descriptor through its own `checkIteratorProperties` and asserts all three attributes, which is what the three excluded rows ("Check keys method", "Check values method", "Check entries method") were. Writable and configurable were already right; enumerable was the whole of it. This is the family, not the row. `HeadersIteratorPrototype`, `FormDataIteratorPrototype` and `UrlSearchParamsIteratorPrototype` each declared a bare `[JsFunction(Name = "next")]`, and no vendored corpus row reaches the latter two — so the two siblings were the same defect with nothing to find it. `ReadableStreamAsyncIteratorPrototype` already carries the flags from sebastienros#3238, which is where `JsFunctionAttribute.Flags` came from. Nothing else on those objects moves: `@@toStringTag` is a class string, which WebIDL also gives `{ [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true }`, and that is what `PropertyFlag.Configurable` already produced. `next.length` is 0 and `next.name` is "next" as before; the declarations now say `Length = 0` explicitly rather than deriving it. The three exclusion rows are deleted, so the driver enforces the fix. While re-reading the `NeedsTriage` doc comment: three of its bullets described the streams defects sebastienros#3195/sebastienros#3238 had already fixed and whose exclusion rows had already gone. Rewritten to say so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE
…erely a disturbed one
`new Response('hi').text()` left `response.body.getReader()` working, where
web-platform-tests' `response-stream-disturbed-5.any.js` asserts a `TypeError` —
eight of its twelve rows, the four whose body source is a `ReadableStream`
passing.
The recorded analysis said the stream `.body` exposes for a byte-source body "is
not the one the consume path locked". Half right, and the wrong half is the
interesting one: the consume path never locked anything, and *disturbed* is not
what `getReader()` refuses. `getReader()` refuses a **locked** stream and only a
locked one — https://streams.spec.whatwg.org/#set-up-readable-stream-default-reader
step 1 — so a disturbed-but-unlocked stream is handed a reader quite correctly.
What makes a browser refuse it is that consuming a body runs *fully read*
(https://fetch.spec.whatwg.org/#concept-body-fully-read), whose step 3 is "let
reader be the result of getting a reader for body's stream"
(https://streams.spec.whatwg.org/#readablestream-get-a-reader, which locks it)
and whose read-all-bytes step never releases that reader. The stream a consumed
body exposes is therefore disturbed, closed *and locked for good*. That is
exactly why the `ReadableStream`-source rows already passed: those go through
`FetchBody.FullyRead`, which acquires the reader and holds it.
`GetOrCreateStream` defers building the stream for a bytes-source body, so the
state it has to reproduce on first ask is the state fully read would have left —
and it was reproducing only the `disturbed` half. It now cancels *and* acquires a
default reader. The stream is built empty in that case: nothing can read a locked
stream, so filling it from the source would copy a body that has already been
handed over only to throw the copy away.
Neighbours checked, and none of them moves: `bodyUsed` was already true before
this (the source-disturbed flag stands in until a stream exists, and `Cancel`
sets `disturbed` afterwards); `clone()` after a consume still throws, because
"body is unusable" is disturbed **or** locked and it was already disturbed;
reading `body` *before* consuming still leaves the body usable and a released
reader still leaves it usable; and the mixin is shared, so a `Request` behaves
the same way — pinned in `RequestTests`. All six consumers are covered by a
theory: `text`, `json`, `arrayBuffer`, `blob`, `bytes`, `formData`.
The two exclusion globs are deleted, so the driver enforces the fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE
…keys included `new Headers(proxy)` performed one operation more than the specification's order allows, on the two rows of `fetch/api/headers/headers-record.any.js` that count them: "Correct operation ordering with two properties one of which has an invalid name" (6 where 5 are allowed) and "Basic operation with Symbol keys" (8 where 7 are). Measured through the file's own logging proxy, the extra operation is the same one in both: a `[[Get]]` of the key that ends the conversion. https://webidl.spec.whatwg.org/#es-record step 4.2 is, per key: 4.2.1 "let typedKey be key converted to an IDL value of type K", *then* 4.2.2 "let value be ? Get(O, key)". `FillFromRecord` did the `Get` first and converted the key inside `append`, so a key that cannot be a `ByteString` — a Symbol, or a string carrying a code unit above 255 — was read before it was refused. Two things follow from writing step 3 as `? O.[[OwnPropertyKeys]]()` rather than as Jint's string-filtered overload. It is what the specification says: a Symbol key is a member of the walk, its descriptor is fetched, and if it is enumerable the conversion of the key is where the `TypeError` comes from. And it removes an inconsistency that was there before: `JsProxy.GetOwnPropertyKeys` honours the `types` filter only when there is no `ownKeys` trap — correctly, since the spec's internal method has no filter — so a Symbol key already reached the walk through a proxy while `new Headers({ [Symbol.toStringTag]: 'x' })` on a plain object was silently accepted. Both now throw, as a browser does. The record is also converted *in full* before the fill appends anything, which is what the two algorithms actually say: the conversion produces a `record<ByteString, ByteString>`, and only then does https://fetch.spec.whatwg.org/#concept-headers-fill run `append` over it. The observable difference is that a record whose second entry has an invalid header *name* no longer leaves the first entry appended. `AppendChecked`/`Validate` keep the `ByteString` conversions for `Headers.prototype.append` and `.set`, where they are WebIDL's argument conversions; the record path calls the converted-string half directly. The two exclusion rows are deleted, so the driver enforces the order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE
… gone stale Item 10 of sebastienros#3212 asked that HTML's multipart/form-data encoding algorithm be read before anything was changed. Reading it settles the question the other way, so nothing is changed and the exclusion stays — with the reason rewritten from "wants a specification read" to what the reading found. HTML's algorithm normalizes the newlines in each entry and then says to "return the byte sequence resulting from encoding the entry list using the rules described by RFC 7578": it defines the *escaping* and delegates the *framing*. RFC 7578 defers to RFC 2046, whose §5.1.1 grammar ends every `multipart-body` with a `close-delimiter` that is not optional. An empty entry list therefore has no shorter conforming encoding than the 50 bytes `MultipartFormData` writes, and nothing licenses the empty body `response-consume-empty.any.js` asks for. Nor does any implementation produce one. Measured on wpt.fyi against the four aligned stable runs (chrome 152, edge 151, firefox 154, safari 26.6), "Consume empty FormData response body as text" is 0/1 in all four while the file's thirteen other rows are 1/1 in all four. So the row keeps its exclusion but `NeedsTriage` is the wrong category for it — that category means "a bug or a specification detail to chase". Giving it one of its own is left to a follow-up rather than done here. Also re-measures the corpus-wide table in `Vendor/README.md`, which five rows had drifted from: Fetch is this change's own (88 -> 75), and Streams (16 -> 11), Compression (84 -> 22), User Timing (9 -> 5) and DOM (2 -> 0, 12 -> 13 files, 62 -> 76 assertions) had gone stale against sebastienros#3195/sebastienros#3238, sebastienros#3243 and sebastienros#3244, which removed their exclusions without revisiting the table. Every per-standard section above it was already right; only the summary and the header count (269/40,617/2,980 -> 270/40,631/2,894) were behind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE
This was referenced Aug 22, 2026
Closed
Merged
lahma
added a commit
that referenced
this pull request
Aug 22, 2026
… [Replaceable] one (#3258) `WebApiRegistration` installed `self` once, for every global, as an ordinary writable data property, and its own comment named the definition it was written against: "HTML exposes `self` through a [Replaceable] accessor pair on Window" (https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-self), which was the only global there was at the time. `WorkerGlobalScope.self` is a plain `readonly attribute WorkerGlobalScope self` with no [Replaceable] (https://html.spec.whatwg.org/multipage/workers.html#dom-workerglobalscope-self), so a worker inherited the window's semantics along with the property and `self = 1` silently replaced the global scope. The two globals genuinely have different IDL and an engine being built does not know yet which one it will be, so the fix keeps Window's shape where it is and gives the worker its own: `WebApiRegistration` records the descriptor it installed and `WorkerGlobalScope.Install` replaces exactly that one, on the worker's global object and on no other. Identity is the test because a *replacement* can only be non-clobbering that way — a `self` the host installed, before the registration through a configuration callback or after it on the built engine, is never the recorded descriptor and is left as the host left it, unread and so unmaterialized. What "refused" means is then decided by the assigning code's own mode rather than by the descriptor: silently ignored in sloppy mode, a TypeError in strict. Both are pinned in `WorkerMechanismTests`, the module body for strict and an indirect `eval` for the sloppy code a module worker cannot otherwise reach, along with `self === globalThis` and `self instanceof DedicatedWorkerGlobalScope` surviving the refusal. `GlobalErrorEventTests` pins the other half — assignment still shadows at top level, in both modes. `DedicatedWorkerGlobalScope.name`, which the triage note called "the same shape and the same question", turned out to be neither: it is `[Replaceable] readonly attribute DOMString name`, so the writable property it already had is what its own IDL asks for. It has a pin of its own now, because the contrast is the point. The two web-platform-tests rows this was filed from do NOT go green, and that is the accounting rather than a shortfall. Both assert what a *sloppy-mode* assignment does, and the worker lane runs its file as a module: `self.any.js` used to fail with `expected object "DedicatedWorkerGlobalScope" but got 1` and now fails with `TypeError`, which is the same refusal in its other voice. They move from `NeedsTriage` to a new `NeedsClassicWorkerScript`, which is the lane (module workers only, divergence 2) and not a defect. `NeedsTriage` is left holding exactly one entry, the empty-`FormData` row that #3247 established is not a defect either, so the category now carries no debt at all — and its doc comment, the runner's, and `Vendor/README.md` say so. Fixes #3224 Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
lahma
added a commit
that referenced
this pull request
Aug 22, 2026
…sTriage empty (#3265) `WptDivergence.NeedsTriage` means "a genuine failure that is not attributable to a feature Jint has decided not to have" -- the category that carries debt. Every defect any corpus filed there has been paid, and what it held was one entry that was never a defect at all: `fetch/api/response/response-consume-empty.any.js`, "Consume empty FormData response body as text". #3247 established the verdict rather than merely suspecting it. HTML's multipart/form-data encoding algorithm defines the escaping and delegates the framing to RFC 7578, which defers in turn to RFC 2046, whose section 5.1.1 grammar ends every multipart-body with a close-delimiter that is not optional -- so an empty entry list has no shorter conforming encoding than the 50 bytes Jint writes. Nor does anything produce the empty body the row asks for: on wpt.fyi's four aligned stable runs it is 0/1 in Chrome, Edge, Firefox and Safari, where the file's thirteen other rows are 1/1 in all four. Add `AssertsWhatNothingRequires` and move the row into it -- the corpus's analogue of test262's `=== PERMANENT EXCLUSIONS ===` banner, earned the same way: only when the standard the test claims to be about does not ask for what the test asks for, and only with a comment recording the decision and the normative citation that makes it defensible, never a to-do nobody intends to do. Age never promotes an entry into it, and reversing one has to argue the decision rather than delete the line. It is the one member not spelled `Needs...`, deliberately: every other names something absent that would turn its rows green if it arrived, whereas here the divergence is in the test and no change to this engine would move it. `NeedsTriage` stays as a named-but-empty category, because its emptiness is now the signal -- a non-zero count there means the corpus has found something, rather than a number a reader has to go and re-read to discover it is one permanent entry. That is legal because nothing enumerates the type: the driver reads `WptExclusion.Divergence` only to name a category in a staleness message, so a member with no entries costs nothing and fails nothing. `NeedsMessageChannel` is the older precedent for exactly this. `NeedsClassicWorkerScript` stays where it is, and its doc now says why it is not this new category despite also being permanent and also not debt. The line is what the standard asks. There, nothing asks for what the test asserts and no implementation delivers it; here HTML asks for exactly what those two `self` rows assert -- a sloppy-mode assignment to a read-only attribute is a silent no-op -- and an implementation that runs the file as the classic `.any.worker.js` script wpt generates satisfies them. What makes them permanent is a lane Jint chose (module workers only), which is the family `NeedsDeclinedWorkerGlobals` and `NeedsForbiddenHeaderNames` are in. Both prose sites claimed that giving this row its own category "is filed rather than done here". Nothing was ever filed until #3261, and this is that change, so both now describe reality. Three further stale claims found while re-reading the two doc comments the issue asks about, each corrected: - `WptTestRunner`'s class doc said the driver has thirty-four suites. #3258 vendored `workers/modules` as a thirty-fifth without moving the number; the census table in `Vendor/README.md` already said 35. - `Vendor/README.md`'s WebCryptoAPI section still said `NeedsTriage` holds ECDH's mismatched-curve rows as open debt. #3183 fixed them and removed their entries without touching that file, so the corpus has named no debt since. - The same section counted "one of six categories" and then gave two different categories the ordinal "seventh". It is five today, with the sixth and seventh being the two that have since emptied. The exclusion table's coverage is untouched: all 196 entries keep their file, name pattern and platform scope, and only one entry's category changed. Nothing in the driver's enforcement reads the category, so the whole corpus is unchanged -- 422/422 WPT theories green before and after, with no row moving in either direction. Full `Jint.Tests` green on both target frameworks. Closes #3261 Claude-Session: https://claude.ai/code/session_01FHnbgQGW5QgfatxL6DgGEd Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
lahma
added a commit
that referenced
this pull request
Aug 23, 2026
…A-262's (#3272) * WebApi: every operation carries WebIDL's property attributes, not ECMA-262's WebIDL gives a regular operation `{ [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true }` (https://webidl.spec.whatwg.org/#es-operations), and the same "define the operations" steps cover a static operation, a stringifier, a `toJSON`, an iterable declaration's `entries`/`keys`/`values`/`forEach`, and a namespace object's members. Enumerable is the half that matters: it is the opposite of ECMA-262's rule for a built-in function property, which is what the source generator defaults to because every host under `Jint/Native/` wants it. 183 `[JsFunction]` declarations across 46 files now opt in. Five already did — the `next`/`return` of the four iterator prototype objects, fixed by #3238 and #3247 when a corpus row happened to assert them — so 188 of 188 operations are now right. Two `[JsAccessor]` declarations join them, `Crypto.prototype.subtle` and `Performance.prototype.timeOrigin`, the last two attributes in the subtree that were not already `Configurable | Enumerable`. This removes an incoherence rather than creating one. The attribute and constant rules were already implemented deliberately — 184 of 187 accessors and all 72 constants were correct — so `Object.keys(Response.prototype)` answered 9 while `getOwnPropertyNames` answered 17, and a `for...in` over a `Response` yielded nine attribute names and not one operation. Node 24 is the oracle throughout and agrees member for member: `SubtleCrypto.prototype.digest`, `TextDecoder.prototype.decode`, `Performance.prototype.now`, `Response.json`, `URL.canParse`, `ReadableStream.from` and `Storage.prototype`'s six are all enumerable there. `navigator` and `scheduler` are deliberately untouched; the next commit says why. `Jint/Native/` is untouched, and test262 is unmoved at 102,495 passed / 0 failed. Cost is zero, measured rather than assumed: `new Engine()` allocates 9,256.0 B, `new Engine(o => o.UseWebApis())` 24,280.0 B, and the same engine with fifteen web-API objects' property tables materialized 60,912.0 B — bit-identical before and after, three rounds each. The flags are one enum argument on the same call in both storage paths, and the one enumerability-gated fast path (`BuiltinShape.FixedSlotsAllNonEnumerable` via `HasNoEnumerableOwnStringKeysCore`) is unreachable here: `HasNoEnumerableOwnStringKeys` is overridden only by `ArrayInstance`, `JsObject` and `ObjectPrototype`. The twenty "one documented simplification against WebIDL: the operations are non-enumerable" paragraphs those host files carried are removed, because they no longer describe anything. What survives is the part of each that is still true — `console`'s and `crypto`'s and `performance`'s data-property install where WebIDL asks for a `[Replaceable]` accessor pair, and `Storage.prototype.length` being a real accessor pair. Closes #3239. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHnbgQGW5QgfatxL6DgGEd * WebApi: navigator and scheduler keep ECMAScript's attributes, and the reason is in the code The one part of the sweep that must not be automated. `NavigatorInstance` and `SchedulerInstance` have no interface object and no interface prototype object, so `userAgent`, `postTask` and `yield` are own properties of the singleton rather than of a prototype. WebIDL's enumerability rule assumes the member is where the specification puts it, and there the enumerability is invisible from the instance: Node 24 reports `userAgent` as an enumerable accessor on `Navigator.prototype` and `Object.keys(navigator)` as `[]`, because the instance has no own properties at all; a browser answers `Object.keys(scheduler)` the same way for the same reason. So flipping these three would make `Object.keys(navigator)` report `["userAgent"]` and `Object.keys(scheduler)` report `["postTask", "yield"]`, which no implementation does. Non-enumerable here is what reproduces the browser answer, and what fixes them properly is the interface object — as #3250 did for `Crypto`, `SubtleCrypto` and `Performance` — not a flag. That is a separate change, and this is exactly why the sweep is a reviewed per-declaration opt-in rather than a namespace-scoped generator default: a blanket flip would move these two objects further from a browser rather than closer. `console` is the counter-example that proves the rule rather than a precedent against it: it is a WebIDL namespace, whose members genuinely are own properties of the namespace object, so its eighteen operations are enumerable here exactly as they are in Node, where `Object.keys(console).length` is 25. Both exemptions are recorded in `WebIdlPropertyAttributeTests.Divergences`, which fails on an entry that has stopped diverging — so whoever gives these two an interface object has to delete the entry rather than inherit a stale exemption. The stale "the same pair console, crypto and performance carry" sentence both files opened with goes too: console got a private prototype in #3266, and crypto and performance got real interface objects in #3250. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHnbgQGW5QgfatxL6DgGEd * Tests: a structural WebIDL attribute matrix, and the nine pins it moves The artefact that keeps the sweep true. `WebIdlPropertyAttributeTests` builds an engine with every web-API feature on, walks every object reachable from the principal realm's global — 131 objects, 816 members — classifies each member by the WebIDL member kind it is, and holds it to that kind's property attributes. Nothing is enumerated by hand, so a member added tomorrow is checked tomorrow; against the previous commits reverted it reports exactly the 185 members that changed, 183 operations and 2 attributes. Four assertions, three of which exist to stop the matrix rotting: * the sweep itself; * every `[JsObject]` host under `Jint/WebApi/` must be visited — found by reflecting for the method the generator emits on one, so the list cannot go stale, with the two worker global scopes recorded as out of reach from the principal realm and re-checked in both directions; * every recorded divergence must still diverge, the WPT exclusion table's discipline: a fix cannot leave an exemption behind; * `Object.keys(navigator)` and `Object.keys(scheduler)` are `[]` while `Object.keys(console)` is not — the property the two divergences rest on, stated where a reader will find it. The class doc is the table to read when adding a member: WebIDL member kind, attributes, and the declaration that produces them, with the spec anchor for each. A green matrix is not a conformance certificate and says so — it checks that a member carries its kind's attributes, never that the member is on the right object, which is the very distinction the two divergences turn on. Nine existing pins move to the browser-correct expectation. Six asserted an operation or an attribute was non-enumerable and now assert the opposite — `CryptoTests.GivesItsMembersTheAttributesWebIdlGivesAnOperation`, `SubtleCryptoTests.IsAReadOnlyEnumerableAttributeOnTheCryptoPrototype`, `SubtleCryptoTests.ExposesAllTwelveOperations`, `SubtleCryptoAesWrapTests.ExposesAllTwelveOperationsWithTheirIdlNameAndArity`, `PerformanceTests.ExposesTimeOriginAsAReadOnlyAccessor`, `EncodingTests.ExposesAttributesAndOperationsWithTheirWebIdlPropertyAttributes` — three of them renamed, because the old names asserted the old answer. Each keeps its `Object.keys(instance)` is `[]` half, which is what shows the enumerability lands on the prototype and not on the object a script holds. `StorageTests.ForInVisitsTheStoredKeysAndTheEnumerableAttribute` now sees the five operations beside `length`, which is what Node 24 sees; the two visit the same six names in a different order, because Node builds its prototype in IDL declaration order where Jint's generator sorts a host's members by JS name. In `Jint.Tests.PublicInterface`, which has no `InternalsVisibleTo` and therefore proves the surface from a third party's side, `WebApiEventTests.GivesEveryMemberTheAttributesWebIdlAsksFor` had pinned the old answer with "changing it is a deliberate act". This is that act — and it gains the `Object.keys(EventTarget.prototype)` reading plus the `constructor` back-reference, which stays non-enumerable and is why that list is three names and not four. One comment correction rides along: `ReadableStreamAsyncIterationTests` said `ReadableStream.prototype[@@asynciterator]` is non-enumerable "because it is an interface member, and those stay non-enumerable". Interface members no longer stay non-enumerable; that symbol is non-enumerable because WebIDL's iterable declarations say so, which is now the whole of the reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHnbgQGW5QgfatxL6DgGEd * Wpt: correct the reason @@asynciterator is non-enumerable in the #3195 triage record The record explaining why the async-iterator corpus row moved ended by noting that `ReadableStream.prototype[@@asynciterator]` was already right, "non-enumerable because it is an interface member rather than an iterator-prototype method". The first half is still true and the reason is not: interface members are enumerable now, and that symbol is non-enumerable because WebIDL's iterable declarations give it `{ [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }`. The corpus itself is untouched and unmoved — 422/422 before and after the sweep, no row in either direction, so no exclusion-table entry changes either. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHnbgQGW5QgfatxL6DgGEd * AGENTS.md: WebIDL's property attributes are the fourth Web APIs convention The subtree already had three conventions written down — the whole-file `#if NET8_0_OR_GREATER`, the install from `Options.Apply` as lazy globals, and the principal realm only. The fourth is now true of every member and worth stating where the next web-API PR will read it: WebIDL's attributes rather than ECMA-262's, the four flag spellings that produce them, the matrix test that enforces them, and — the part that is not mechanical — why this is deliberately not a namespace-scoped generator default, with `navigator` and `scheduler` named as the two objects a blanket flip would get wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHnbgQGW5QgfatxL6DgGEd --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Fixes items 7, 8 and 9 of #3212, and returns a verdict on item 10 rather than a fix. With #3243, #3244 and #3245 merged, this closes out the eleven — #3212 can close with this.
13 corpus rows go green; exactly 13 rows move across the whole 40,631-assertion corpus, all FAIL→PASS, verified row by row.
Item 7 — iterator prototype
next— and two siblings nobody was watchingWebIDL §3.7.9.2 requires
nextto be{ writable, enumerable, configurable }. Onlyenumerablewas wrong;writable,configurable,length0,nameand the@@toStringTagshape were already right, and the new tests pin all of them so the next change to that object cannot quietly move one.Headershad the failing rows, but the same defect was inFormDataandURLSearchParamsiterator prototypes too — and no vendored row reaches either, so it was invisible debt. Fixed all three; that is the entire change, one attribute per file, made expressible by #3238'sFlagsproperty.Item 8 — the recorded cause was wrong in its operative half
It said the stream
.bodyexposes "is not the one the consume path locked". The consume path locked nothing, anddisturbedis not whatgetReader()refuses: SetUpReadableStreamDefaultReader step 1 refuses a locked stream and only a locked one, so Jint handing a reader to a disturbed-but-unlocked stream was quite correct.What a browser does is fully read step 3 — "let reader be the result of getting a reader for body's stream" — whose read-all-bytes never releases it. So a consumed body's stream is disturbed, closed and locked for good. That also explains why the
ReadableStream-source rows already passed: they go throughFetchBody.FullyRead, which holds its reader.GetOrCreateStreamnow cancels and acquires a default reader, building the stream empty since nothing can read a locked one.Item 9 — the extra operation, and a pre-existing inconsistency it exposed
Measured through the WPT file's own logging proxy, the extra operation is the same on both rows: the
[[Get]]of the key that ends the conversion. WebIDL §3.2.23 step 4.2 is typedKey conversion then? Get(O, key);FillFromRecorddid theGetfirst and converted the key insideappend.Writing step 3 as the unfiltered
? O.[[OwnPropertyKeys]]()removed a real inconsistency along the way:JsProxy.GetOwnPropertyKeyshonours Jint'stypesfilter only when there is noownKeystrap — correctly, since the spec's internal method has no filter — so a Symbol key already reached the walk through a proxy whilenew Headers({ [Symbol.toStringTag]: 'x' })on a plain object was silently accepted where a browser throws.Measured before → after: invalid-name row 6 → 5 operations, Symbol-keys row 8 → 7; the three already-correct shapes unchanged.
Item 10 — not a defect. Row left in place.
HTML's algorithm normalizes newlines per entry and then returns "the byte sequence resulting from encoding the entry list using the rules described by RFC 7578" — it defines the escaping and delegates the framing. RFC 7578 defers to RFC 2046, whose §5.1.1 grammar ends every
multipart-bodywith aclose-delimiterthat is not optional. An empty entry list has no shorter conforming encoding than the 50 bytes Jint writes.The empirical half is decisive: on wpt.fyi across four aligned stable runs (chrome 152, edge 151, firefox 154, safari 26.6), "Consume empty FormData response body as text" is 0/1 in all four, while the file's thirteen other rows are 1/1 in all four (methodology validated against three control rows). No implementation passes it.
The row stays excluded, and its prose in
WptExclusions.csandVendor/README.mdnow records that verdict instead of "wants a spec read".NeedsTriageis the wrong category for it — it wants one meaning "the test asserts what nothing requires", the WPT analogue of test262'sPERMANENT EXCLUSIONS. Recommended as a separate issue rather than invented here.Item 8's neighbours, all pinned
clone()after consumption still throws (unusable is disturbed or locked, and it was already disturbed), including withbodyread first;bodyUsedtiming unchanged;Requestcovered as well asResponse, with its new test watched failing against unfixed code (source reverted, run, restored); all six consumers as a theory —text/json/arrayBuffer/blob/bytes/formData,bytes()beyond the four the issue named; and the opposite direction, wheregetReader()+releaseLock()leaves the body usable and consuming then locks that same stream.Exclusion accounting
13
NeedsTriagerows deleted — 3headers-basic, 2headers-record, 2 globs covering 8 rows ofresponse-stream-disturbed-5. TwoNeedsTriageentries remain repo-wide: the workersself = 1row (#3224) and the empty-FormData row above. The doc comment now has exactly two live bullets.Doc-staleness caught, as warned: three
NeedsTriagebullets still described the streams defects that #3238 had fixed and whose rows were already gone. Rewritten to past tense.Verification
Solution builds Release, 0 errors, all TFMs.
Jint.Tests10275 (net10.0) / 7147 (net472);Jint.Tests.PublicInterface2405 / 1879; both repeated withJINT_HOST_CONTRACT_VERIFICATION=1(2409 / 1883). WPT 420/420 after rebase.Row-level census across all 270 files, before and after:
Exactly 13 rows moved, all FAIL→PASS, and they are exactly the 13 intended. Zero rows changed in the other direction, none appeared or disappeared, and no other corpus moved by a single assertion. Stated because it is a finding rather than a rounding error.
Every engine change is inside
Jint/WebApi/, soJint.Tests.Test262was not run.Left undone
[JsFunction]declarations underJint/WebApi/are non-enumerable, where WebIDL §3.7.6 gives a regular operation{ writable, enumerable, configurable }. No vendored row catches it (idlharnessis not vendored). Already filed as Every web-API operation is non-enumerable, where WebIDL makes regular operations enumerable #3239; deliberately out of scope here.FillFromSequencestill interleaves conversion and fill — the same shape item 9 fixed on the record arm. No vendored row counts it; noted rather than changed.Vendor/README.mdcorpus table was stale onmainfor rows unrelated to this work; re-measured and corrected rather than leaving known-wrong figures beside new ones.🤖 Generated with Claude Code