fetch: remove unsafe from FetchTasklet and fetch.rs
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:
FetchTaskletis JS-thread-only now:#[derive(CellRefCounted)],&self+Cell/JsCell,ThisPtrentry points holdingref_guard(), typed slots (progress_ref/http_ref/request_stream_ref) replacing the threeref_()/deref()pairs,impl Dropinstead ofdeinit+heap::take. Everything the HTTP thread touches lives in anArc<FetchShared>(Guarded<SharedState>, signalStore, atomics, the tasklet address as an inertTask), which is thebun_httpresult handler and the request-body drain handler and posts three task tags back (FetchTaskletprogress,FetchTaskletHandBack,FetchTaskletRequestDataDrain); hand-back is posted before the terminal progress task.bun_http:OwnedRequest<S>(anAsyncHTTPplus 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 storeshanded_backon the caller's original right before the terminal callback / shutdown release),trait HTTPClientResultHandler+HTTPClientResultCallback::from_handler(Arc<H>)(no&AsyncHTTPreaches the handler, so a reclaimed request can't be read through it),ThreadSafeStreamBufferwith a real lock guard andtrait DrainHandler,preconnect_owned(Box<[u8]>)replacing theis_url_ownedfree-by-hand.bun_jsc:AbortSignal::listen_native(BackRef<C, Root>) -> AbortListenerRegistration(Drop unregisters + unrefs),AbortSignal::retain,unsafe trait WeakOwner+weak_owner!forWeak::create_for,JSValue::as_direct_class_ref,HeadersRef+create_from_pico_headersinfetch_headers.Bun__FetchResponse_finalize/Bun__FetchTasklet__onResolve|RejectRequestStreamare 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 forHTTPResponseMetadata: 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.