Repository navigation
Add opt-in fetch - #3110
Merged
Merged
Add opt-in fetch#3110
Conversation
The algorithm on top of the object model: a global fetch(input, init) behind WebApiFeatures.Fetch, which UseWebApis() never sets. FetchOperation is patterned on ModuleLoadCompletion and obeys the same rules. The policy checks and the Request construction run on the engine thread and every failure is a rejection, so fetch() never throws. The event-loop generation and the realm are captured at registration; the HTTP work runs on a pool thread against a snapshot that mentions no engine type; the outcome is classified off-thread into plain CLR data and a generation-stamped job builds the Response — or the error — on the engine thread, in the captured realm. The settle is once, by CAS. Cancellation is classified rather than flattened: the request's AbortSignal rejects with its own reason, the Fetch.Timeout budget rejects with a TimeoutError DOMException, and the engine's CancellationConstraint settles nothing at all — a constraint that became a rejection would no longer bound anything. A RestoreGlobalSnapshot cancels every request in flight, so the socket is let go at once while the promise stays pending behind the fence. Redirects are driven here rather than by HttpClient, with AllowAutoRedirect off, so every hop re-runs the scheme list and the host's UrlFilter: a server answering 302 Location: http://169.254.169.254/ cannot launder the request past a first-hop check. The loop honours redirect: follow/error/manual, rewrites 303 — and 301/302 on POST — to GET while dropping the body and its content headers, strips Authorization/Cookie/Proxy-Authorization across an origin change, and caps the hop count. The body is read under MaxResponseBytes as a running total over the decompressed bytes, so a lying Content-Length and a chunked compression bomb are both caught. Every network-class failure is one TypeError saying only "Failed to fetch", so a script cannot map the host's network by reading the failures apart; the originating CLR exception rides the error value for the host to read with JintException.TryGetClrException. MaxConcurrentRequests is enforced at the call and the excess is refused rather than queued. ModuleLoadCompletion.MustPropagate moves to Runtime/ConstraintFailure.cs so that fetch's settle job and the module loader's cannot drift apart. THREAT_MODEL.md gains TM-21 for the SSRF, response-size and concurrency threats, cross-referencing TM-11; README gains a fetch section, its security paragraph and the table row. Co-Authored-By: Claude Fable 5 <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.
Adds the fetch algorithm on top of the object model from #3101:
fetch(input, init)as an opt-in global, driven entirely by the engine's own event loop.How a request runs
FetchOperationfollows theModuleLoadCompletionpattern. The synchronous phase (argument coercion, scheme +UrlFilterpolicy, pre-aborted signal) runs on the engine thread and produces a rejection, never a throw. The network phase runs on the thread pool viaSendAsync(..., ResponseHeadersRead)and never touches the engine: the outcome is classified off-thread into a plain CLR record, then enqueued as a generation-stamped job that enters the captured realm, builds theResponse(or error) there, and settles the capability. A settle that arrives afterRestoreGlobalSnapshotis discarded by the same generation fence every other cross-thread completion sits behind, and the restore path cancels the in-flight requests' CTSes so their sockets are freed rather than leaked.ConstraintFailure.MustPropagateis hoisted out ofModuleLoadCompletionso both completion paths share one definition of "a constraint failure never becomes a rejection".Transport and policy
FetchTransportowns the HTTP mechanics: a lazily-created sharedSocketsHttpHandlerclient (AllowAutoRedirect = false,UseCookies = false, decompression on) unless the host suppliesHttpClientor a per-requestHttpClientFactory, and a manual redirect loop so every hop re-runs the scheme check andUrlFilter— a redirect to169.254.169.254fails policy exactly like a direct request (the SSRF shape TM-21 documents). Cross-origin hops stripAuthorization/Cookie/Proxy-Authorization; 301/302/303 rewrite the method per spec;redirect: 'follow' | 'error' | 'manual'follows undici's server-side semantics. The response body is a bounded manual read of the decompressed bytes (MaxResponseBytes), so an oversized or lyingContent-Lengthends in a cleanTypeErrorrejection.Failure map: policy/DNS/connection/TLS/redirect-cap/size-cap →
TypeErrorwith the CLR exception attached to the error value (readable viaTryGetClrException); abort → the signal's reason; the default 30 s timeout →TimeoutErrorDOMException(armed withCancelAfter, so it fires even when nobody pumps); an engineCancellationConstraint→ no settlement at all.Bounds
Defaults:
https/httponly, 32 MB response cap, 20 redirects, 30 s timeout, 10 concurrent requests per engine, header name/value validation (CR/LF/NUL →TypeError) as request-splitting defense.credentials/cache/mode/referrer/integrityare accepted and ignored, per the server-side runtime convention.WebApiFeatures.Fetchis never part ofDefault— it impliesEvents | Url | Filesbut is only ever enabled by naming it or callingUseFetch(...)..github/THREAT_MODEL.mdgains TM-21 covering the egress surface.Tests
Jint.Tests/Runtime/WebApi/FetchTests.csandJint.Tests.PublicInterface/WebApiFetchTests.cs(stub-handlerHttpClient, no sockets): policy refusal per hop, redirect semantics and header stripping, size cap, timeout and abort mid-flight, restore-fence-drops-the-settle-and-cancels-HTTP, constraint-never-rejects, and the full failure map. Both suites green on net10.0 and net472, with and withoutJINT_HOST_CONTRACT_VERIFICATION=1.🤖 Generated with Claude Code
https://claude.ai/code/session_011G7Ud48VAs1JicxRZxLDxg