Skip to content

Add the fetch object model: Headers, Request and Response - #3101

Merged
lahma merged 1 commit into
sebastienros:mainfrom
lahma:webapi/fetch-model
Aug 20, 2026
Merged

lahma merged 1 commit into
sebastienros:mainfrom
lahma:webapi/fetch-model

Conversation

@lahma

@lahma lahma commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Part of the opt-in web API series (#3079): the fetch object model — Headers, Request and Response behind WebApiFeatures.Fetch — with no networking in this PR. The algorithm (transport, redirect policy, the actual fetch() global doing I/O) follows as the next PR; splitting them keeps this one fully offline-testable and reviewable as pure object semantics.

  • Headers per https://fetch.spec.whatwg.org/#headers-class: pair-sequence/record init, append/delete/get/getSetCookie/has/set, iteration as the spec's sort-and-combine recomputed live per step (mutation-pinned: snapshotting the sort once fails IterationIsLive), multi-value backing so Set-Cookie never folds, RFC 9110 token validation for names and NUL/CR/LF refusal for values — the request-splicing defense is mutation-pinned too.
  • Request (minimal per the plan): normalized method with the forbidden-method list enforced (CONNECT/TRACE/TRACK — unlike the browser's forbidden-header list, which is deliberately not enforced server-side, the Node/Deno posture; both decisions documented on the types), URL resolved by the in-tree WHATWG parser with no base (an embedded engine has no document, so relative input is a TypeError; URLs with credentials refused per spec), signal, redirect, body mixin, clone().
  • Response: constructor with initialize a response in the spec's exact step order (body extracted before the range checks — Response.json(null, {status: 204}) is a TypeError, initially implemented wrong and caught by the pinned test), statics error()/json() (via Jint's own JSON serializer)/redirect(), text()/json()/arrayBuffer()/bytes()/blob(), clone(), type as basic/default/error (no origin → no cors/opaque). Bodies are buffered byte[] in this PR; body answers null (a meaningful spec value — if (response.body) degrades sanely) and the streaming integration lands separately on top of the streams feature. FormData bodies are a TypeError naming the missing multipart serializer, which also arrives separately.
  • WebApiFeatures.Fetch = 1 << 10 implies Events | Url | Files (closure computed at install, pinned) and is never part of Default — UseWebApis() still yields an engine with no fetch-anything; UseFetch() is the explicit opt-in and arrives with the algorithm PR.

Verified: all-TFM build, both suites both frameworks, both host-contract-verification configurations; seven mutation-verified pins across this and the algorithm commit (the ones in this PR: live iteration, header-value splicing, Response.json null-body ordering).

🤖 Generated with Claude Code

The offline half of fetch — everything a host can adopt without granting
outbound network access. Behind the new WebApiFeatures.Fetch flag, which
UseWebApis() never sets and Default will never include.

Headers is the whole class: append/delete/get/getSetCookie/has/set, the
iterable declaration's entries/keys/values/forEach/@@iterator over sort-and-
combine (distinct lowercased names in byte order, values joined by ", ",
Set-Cookie the one name never combined), RFC 9110 token names and header
values that may carry no NUL, CR or LF — the request-splitting defence. The
list itself is engine-free, so the HTTP half can build one off-thread.

Request is the minimal request of the plan's surface v1: method (normalized
for the six standard methods, forbidden methods refused), url, headers,
redirect, signal, the Body mixin and clone. Response is status/ok/statusText/
headers/url/redirected/type/bodyUsed, text/json/arrayBuffer/bytes/blob/clone
and the three statics error/redirect/json.

Bodies are byte[]-buffered rather than streamed: every consumer answers an
already-resolved promise, bodyUsed flips on the first read, a second read
rejects with a TypeError, clone-after-read throws one, and a clone shares the
bytes while carrying its own flag. body is therefore always null and says so.
A FormData body is a TypeError naming the missing multipart serializer rather
than a silently stringified object.

Enabling Fetch implies Events, Url and Files, whose interfaces are part of
fetch's own surface; the closure is computed in WebApiRegistration so that a
host assigning Options.WebApi.Features directly gets it too, and so that the
option keeps reading back what the host asked for.

Options.FetchOptions arrives with the flag — HttpClient/HttpClientFactory,
AllowedSchemes, UrlFilter, MaxResponseBytes, MaxRedirects, Timeout and
MaxConcurrentRequests — so the policy surface is reviewable before anything
uses it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lahma
lahma merged commit f9ab8a3 into sebastienros:main Aug 20, 2026
5 checks passed
@lahma lahma mentioned this pull request Aug 20, 2026
@lahma
lahma deleted the webapi/fetch-model branch August 20, 2026 19:42
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