fetch: remove unsafe from FetchTasklet and fetch.rs

JA
2mo ago
+2669-2066

What

Same programme as #40055 / #40135 / #40136 / #40139 / #40187 / #40190 / #40200, applied to the fetch client: src/runtime/webcore/fetch/FetchTasklet.rs 61 → 0, fetch.rs 8 → 0 (incidental: Response.rs 30→21, FetchRequestBodySink.rs 6→4, ReadableStream.rs 23→21). Stacked on #40192 (BackRef<T, Root>), whose commit is included here.

The cross-thread hand-off is the substance:

  • FetchTasklet is JS-thread-only now: #[derive(CellRefCounted)], &self + Cell/JsCell, ThisPtr entry points holding ref_guard(), typed slots (progress_ref / http_ref / request_stream_ref) replacing the three ref_()/deref() pairs, impl Drop instead of deinit + heap::take. Everything the HTTP thread touches lives in an Arc<FetchShared> (Guarded<SharedState>, signal Store, atomics, the tasklet address as an inert Task), which is the bun_http result handler and the request-body drain handler and posts three task tags back (FetchTasklet progress, FetchTaskletHandBack, FetchTaskletRequestDataDrain); hand-back is posted before the terminal progress task.
  • bun_http: OwnedRequest<S> (an AsyncHTTP plus the boxed storage it borrows — URL, header buffer, body bytes; HRTB constructor/mutator so borrows can't escape), InFlight<S> (!Send; reclaim() succeeds only after the HTTP thread stores handed_back on the caller's original right before the terminal callback / shutdown release), trait HTTPClientResultHandler + HTTPClientResultCallback::from_handler(Arc<H>) (no &AsyncHTTP reaches the handler, so a reclaimed request can't be read through it), ThreadSafeStreamBuffer with a real lock guard and trait DrainHandler, preconnect_owned(Box<[u8]>) replacing the is_url_owned free-by-hand.
  • bun_jsc: AbortSignal::listen_native(BackRef<C, Root>) -> AbortListenerRegistration (Drop unregisters + unrefs), AbortSignal::retain, unsafe trait WeakOwner + weak_owner! for Weak::create_for, JSValue::as_direct_class_ref, HeadersRef + create_from_pico_headers in fetch_headers. Bun__FetchResponse_finalize / Bun__FetchTasklet__onResolve|RejectRequestStream are HOST_EXPORTs (promise-reaction shape).
  • New unsafe impl Send/Sync: bun_event_loop::Task (inert tag+address), ThreadSafeStreamBuffer (fields guarded by its mutex), bun_picohttp::Header (two borrowed byte views; needed for HTTPResponseMetadata: Send).

Per-fetch cost vs main (native mallocs per fetch, N=2000): GET 12.58 → 12.46, POST string 12.48 → 12.46, GET + 3 headers 16.22 → 15.13; progress posts reuse an inline ReusableConcurrentTask (zero allocs per tick), the hand-back rides the terminal progress update, OwnedRequest is one allocation, the in-flight ticket is lock-free. Pre-existing issue fixed in passing: the Response body's producer back-pointer dangled after tasklet teardown; it is now cleared (detach_response_body_producer).

Testing

Debug+ASAN: fetch.test.ts 359/364 (root-permission Bun.file cases + the 150 ms AbortSignal.timeout redirect case, identical against a release server), fetch.stream 117, fetch-gzip 80, fetch.tls 30, fetch-tls-abortsignal-timeout, fetch.upgrade, abort-signal-leak, body-stream 9086, body*, fetch-abort-*, fetch-backpressure, fetch-http2-client/adversarial/leak, fetch-keepalive, fetch-preconnect, proxy tests, fetch-redirect, fetch-response-finalizer-sweep, fetch-stream-cancel-leak, fetch-syscall-fault, fetch.unix, fetch.tls.wildcard, response, stream-fast-path; fetch-leak 28/29 (the URLSearchParams variant exceeds its 120 s budget on debug; standalone RSS ratio 1.5 ≪ 10); serve.test.ts, offline s3 suite, node-fetch tests, node parallel fetch tests. Hand-driven against a local server: GET/POST/blob, partial read + cancel, abort before headers / mid-body / after, ReadableStream request body, redirect chain, unread/GC'd bodies, 500 concurrent + GC, worker terminated mid-fetch — exit 0, no ASAN output, tasklets created == dropped (612/612). clippy clean on every touched crate.