This page documents Bun's implementation of the Web fetch() API and the underlying HTTP client stack. It covers the native fetch function, request/response body handling, streaming, redirect behavior, TLS configuration, proxy support, and connection pooling.
For the HTTP server (Bun.serve()), see page 5.1 For WebSocket client/server support, see 5.3 For the node:http server side (http.createServer), see 8.3 For the raw socket layer, see 5.5
Bun's HTTP client stack is layered across JavaScript, Rust, and Zig. The JS fetch() global hands off to a Rust FetchTasklet, which drives a Zig HTTPClient on a dedicated background thread.
Fetch call stack — from JS to network
Sources: src/runtime/webcore/fetch/FetchTasklet.rs1-50 src/http/http.zig1-55 src/http/HTTPThread.zig1-50 src/http/HTTPContext.zig1-35 src/http/lib.rs1-90
Key code entities and their roles
Sources: src/runtime/webcore/fetch/FetchTasklet.rs1-40 src/http/AsyncHTTP.zig1-35 src/http/http.zig1-15 src/http/HTTPThread.zig1-50 src/http/HTTPContext.zig1-95 src/http/ProxyTunnel.zig1-30
fetch() APIBun implements the WHATWG Fetch specification and extends it with several Bun-specific options.
| Option | Type | Description |
|---|---|---|
method | string | HTTP method (default "GET") |
headers | HeadersInit | Request headers |
body | BodyInit | Request body. GET/HEAD/OPTIONS reject a body. |
redirect | "follow" | "manual" | "error" | Redirect behavior |
signal | AbortSignal | Abort / timeout signal |
keepalive | boolean | HTTP connection keep-alive and pooling |
| Option | Type | Description |
|---|---|---|
tls | object | Per-request TLS settings (see below) |
proxy | string | URL | Explicit proxy URL |
unix | string | Connect to a Unix domain socket path |
decompress | boolean | Auto-decompress response (default true) |
verbose | boolean | Log request/response details to stderr |
These options are tested in test/js/web/fetch/fetch.test.ts test/js/web/fetch/fetch.tls.test.ts and test/js/bun/http/proxy.test.ts
fetch() accepts a string, URL, Request, or any object with a .toString() method returning a URL. data: URLs are also supported (base64 and plain text variants).
fetch("https://example.com")
fetch(new URL("https://example.com"))
fetch(new Request("https://example.com"))
fetch("data:text/plain,Hello%2C%20World!")
Sources: test/js/web/fetch/fetch.test.ts56-195
Fetch accepts the standard BodyInit types: string, Blob, ArrayBuffer, TypedArray, FormData, URLSearchParams, and ReadableStream.
ReadableStream body that has already been locked (.getReader() called) causes fetch to reject with ERR_STREAM_CANNOT_PIPE.Sources: test/js/web/fetch/fetch.test.ts666-690 test/js/web/fetch/fetch.stream.test.ts152-175
The Response object exposes standard Web API body consumption methods:
| Method | Return type | Notes |
|---|---|---|
.text() | Promise<string> | UTF-8 decoded |
.json() | Promise<any> | Parsed JSON |
.arrayBuffer() | Promise<ArrayBuffer> | Raw bytes |
.blob() | Promise<Blob> | Typed blob |
.formData() | Promise<FormData> | Multipart / URL-encoded |
.body | ReadableStream | null | Streaming access |
Once one body consumption method is called, response.body is locked. Attempting to get a reader after buffering has started throws "ReadableStream is locked".
Sources: test/js/web/fetch/fetch.stream.test.ts82-150
response.body is a ReadableStream<Uint8Array>. Chunks are delivered as they arrive from the network via FetchTasklet's ResumableFetchSink.
Streaming data path
Sources: src/runtime/webcore/fetch/FetchTasklet.rs1-124 src/http/http.zig1-55
The redirect option controls redirect behavior:
| Value | Behavior |
|---|---|
"follow" (default) | Automatically follows up to the max redirect count. response.redirected is true if any redirects occurred. |
"manual" | Returns the 3xx response as-is. response.redirected is false. |
"error" | Throws a TypeError with code: "UnexpectedRedirect" if a redirect is returned. |
Internally, redirect chains are tracked via AsyncHTTP.redirected src/http/AsyncHTTP.zig18 and FetchRedirect in FetchTasklet. On cross-origin redirects, the Host header and TLS verification hostname are both re-derived from the redirect target URL—not carried over from the previous hop.
Sources: test/js/web/fetch/fetch.test.ts560-627 test/js/web/fetch/fetch.tls.test.ts32-88
tls OptionThe tls option object controls per-request TLS behavior:
| Field | Type | Description |
|---|---|---|
ca | string | Buffer | string[] | Custom CA certificate(s) |
rejectUnauthorized | boolean | Reject invalid/self-signed certs (default true) |
checkServerIdentity | (hostname, cert) => Error | undefined | Custom certificate identity check |
serverName | string | Override TLS SNI hostname |
The global default for rejectUnauthorized can be controlled with NODE_TLS_REJECT_UNAUTHORIZED=0.
The hostname used for TLS SNI and certificate verification is resolved by getTlsHostname() src/http/http.zig58-78 with this priority:
tls.serverName / tls_props.server_nameclient.hostname (from the Host header, port stripped)client.url.hostnameOn a cross-origin redirect, the hostname is re-derived from the redirect target URL.
checkServerIdentity FlowWhen checkServerIdentity is provided, the HTTP thread sends CertificateInfo to FetchTasklet before any request bytes are written. If the callback returns an Error, the connection is torn down without transmitting the request.
Sources: test/js/web/fetch/fetch.tls.test.ts91-380 src/runtime/webcore/fetch/FetchTasklet.rs1-50
HTTPThread caches custom SSL contexts (keyed by interned SSLConfig pointer) in custom_ssl_context_map src/http/HTTPThread.zig14 The cache holds up to 60 entries with a 30-minute TTL.
Sources: src/http/HTTPThread.zig1-16
proxy OptionThe proxy URL is passed through to HTTPClient.http_proxy src/http/AsyncHTTP.zig13 Only http:// and https:// proxy protocols are supported; others throw code: "UnsupportedProxyProtocol".
node:_http_agent reads proxy configuration from environment variables via parseProxyConfigFromEnv src/js/node/_http_agent.ts2:
| Variable | Applies to |
|---|---|
HTTP_PROXY / http_proxy | HTTP requests |
HTTPS_PROXY / https_proxy | HTTPS requests |
NO_PROXY / no_proxy | Bypass list (comma-separated) |
For HTTPS targets routed through an HTTP proxy, Bun establishes a CONNECT tunnel managed by ProxyTunnel src/http/ProxyTunnel.zig The tunnel is preserved in the keepalive pool (PooledSocket.proxy_tunnel) so subsequent requests to the same origin can reuse the established tunnel.
Sources: src/http/ProxyTunnel.zig1-30 src/http/HTTPContext.zig12-32 test/js/bun/http/proxy.test.ts1-135
NewHTTPContext src/http/HTTPContext.zig manages keepalive connection pools. There are two global context instances on HTTPThread: http_context (plaintext) and https_context (TLS). Custom per-request TLS configs get their own NewHTTPContext instances, cached in custom_ssl_context_map.
Each entry in the pool is a PooledSocket src/http/HTTPContext.zig3-32:
| Field | Purpose |
|---|---|
hostname_buf / hostname_len | Identifies which host owns this socket |
port | Target port |
ssl_config | The SSLConfig this socket was created with |
proxy_tunnel | Established CONNECT tunnel (if any) |
target_hostname / target_port | Origin behind the tunnel |
proxy_auth_hash | Prevents cross-credential pool reuse |
h2_session | HTTP/2 session state (HPACK, server SETTINGS) |
Pool size is 64 sockets per context src/http/HTTPContext.zig3
keepalive OptionSetting keepalive: false in fetch() skips connection pool reuse for that request. The Agent.keepAlive property in node:http maps directly to this flag.
Sources: src/http/HTTPContext.zig1-95 test/js/web/fetch/fetch-tcp-keepalive.test.ts1-30
fetch() accepts a signal option (AbortSignal or the result of AbortSignal.timeout(ms)).
fetch() is called, the promise rejects immediately with DOMException("The operation was aborted.", "AbortError").FetchTasklet cancels the AsyncHTTP via Signals and rejects the promise.AbortSignal.timeout(ms) produces a TimeoutError (DOMException("The operation timed out.", "TimeoutError")).Sources: test/js/web/fetch/fetch.test.ts197-390
HTTPThread src/http/HTTPThread.zig runs on a dedicated background thread separate from the JS event loop. It communicates with the JS thread via lock-free queues.
| Queue | Purpose |
|---|---|
queued_tasks | New AsyncHTTP requests to start |
deferred_tasks | Requests waiting because active_requests >= max_simultaneous_requests |
queued_shutdowns | Abort signals for in-flight requests |
queued_writes | Body data chunks to stream to in-flight requests |
queued_response_body_drains | Back-pressure release signals |
MAX_SIMULTANEOUS_REQUESTS limits total concurrent connections. If the limit is hit, new requests queue into deferred_tasks and are started as active requests complete.
HTTP idle timeout defaults to 300 seconds (idle_timeout_seconds) src/http/http.zig35 configurable via BUN_CONFIG_HTTP_IDLE_TIMEOUT.
Max header size defaults to 16KB (max_http_header_size) src/http/http.zig21 configurable via setMaxHTTPHeaderSize().
Sources: src/http/HTTPThread.zig1-55 src/http/http.zig1-45
Bun automatically decompresses gzip, br (Brotli), deflate, and zstd response bodies using libdeflate (lazy-initialized as lazy_libdeflater on HTTPThread) src/http/HTTPThread.zig46
Pass decompress: false in the fetch options to receive raw compressed bytes. The content-encoding header is still present on the response in that case.
Sources: test/js/web/fetch/fetch-gzip.test.ts1-50 test/js/node/http/node-http.test.ts1124-1146
node:http / node:httpsThe node:http module src/js/node/http.ts re-exports:
| Export | Implementation file |
|---|---|
request() / get() | src/js/node/_http_client.ts |
ClientRequest | src/js/node/_http_client.ts |
Agent / globalAgent | src/js/node/_http_agent.ts |
Server / ServerResponse | src/js/node/_http_server.ts |
IncomingMessage | src/js/node/_http_incoming.ts |
OutgoingMessage | src/js/node/_http_outgoing.ts |
ClientRequestClientRequest src/js/node/_http_client.ts80 implements the Node.js http.request() interface on top of Bun's native fetch via the internal nodeHttpClient Zig function src/js/node/_http_client.ts13:
const nodeHttpClient = $newZigFunction("fetch.zig", "nodeHttpClient", 2);
Key behaviors:
write() is called before end(), the request starts in duplex mode so the server can respond while the body is still uploading.write() is deferred by one tick to allow write(chunk); end() in the same tick to take a non-duplex code path.AbortController is wired up at fetch start so req.abort(), req.destroy(), and options.signal all cancel the same in-flight request.req.setTimeout(ms) uses the same AbortController mechanism.AgentAgent src/js/node/_http_agent.ts maps Node.js agent properties to fetch options:
| Agent property | Effect on fetch |
|---|---|
keepAlive: true/false | Enables/disables connection pooling |
proxy | Forwarded as fetch proxy option |
maxSockets | Defaults to Infinity |
Proxy environment variables (HTTP_PROXY, HTTPS_PROXY, NO_PROXY) are read once via parseProxyConfigFromEnv and stored as kProxyConfig on the agent src/js/node/_http_agent.ts2
Node.js http compatibility stack
Sources: src/js/node/http.ts1-71 src/js/node/_http_client.ts1-400 src/js/node/_http_agent.ts1-50
HTTP/2 for the client is gated behind --experimental-http2-fetch src/http/http.zig14 and tracked in h2_session on pooled connections src/http/HTTPContext.zig31 When negotiated via ALPN, the connection is handed to the H2 session manager (H2.ClientSession). HTTP/2 server push is not exposed to JS.
For HTTP/2 via node:http2, see 5.7
fetch("data:...") is handled at the JS layer before any network request is issued. Supported formats:
data:text/plain;base64,<base64> — decoded bodydata:,<text> — percent-decoded text"failed to fetch the data URL"Sources: test/js/web/fetch/fetch.test.ts64-195
| Parameter | Default | How to change |
|---|---|---|
max_http_header_size | 16 KB | http.setMaxHTTPHeaderSize(n) or BUN_DEFAULT_MAX_HTTP_HEADER_SIZE |
idle_timeout_seconds | 300 s | BUN_CONFIG_HTTP_IDLE_TIMEOUT env var |
MAX_SIMULTANEOUS_REQUESTS | (compile-time constant) | Not user-configurable |
| SSL context cache size | 60 entries | Not user-configurable |
| SSL context cache TTL | 30 minutes | Not user-configurable |
| Connection pool size | 64 sockets/context | Not user-configurable |
Sources: src/http/http.zig21-37 src/http/HTTPThread.zig12-13 src/http/HTTPContext.zig3
Refresh this wiki
This wiki was recently refreshed. Please wait 2 days to refresh again.