oven-sh/bun
HTTP stack
Bun ships its own HTTP/1.1, HTTP/2, HTTP/3, and WebSocket implementations rather than wrapping Node's http module. The client side lives in src/http/; the server side is concentrated in src/bun.js/api/server.zig. Both build on top of uSockets and BoringSSL.
Client side: src/http/
| File | Purpose |
|---|---|
src/http/HTTPThread.zig |
The dedicated HTTP thread (~30 KB). Owns one uSockets loop, runs all outgoing requests, replies cross-thread to the main loop. |
src/http/HTTPContext.zig |
Per-host SSL/TCP context (~38 KB). Wraps us_socket_context_t. |
src/http/AsyncHTTP.zig |
One outgoing request (~17 KB). State machine: connect → send → recv → done. |
src/http/InternalState.zig |
Per-request state shared with C-side parsers. |
src/http/Headers.zig |
Request/Response headers. WHATWG-compatible. |
src/http/Method.zig |
HTTP methods enum. |
src/http/HeaderBuilder.zig, HeaderValueIterator.zig |
Header building/iteration. |
src/http/Decompressor.zig |
gzip/deflate/brotli/zstd response bodies (uses vendored libdeflate, brotli, zstd). |
src/http/Encoding.zig, FetchCacheMode.zig, FetchRedirect.zig, FetchRequestMode.zig |
fetch() enums. |
src/http/H2Client.zig, H2FrameParser.zig, h2_client/ |
HTTP/2 client. Uses lshpack for HPACK. |
src/http/H3Client.zig, h3_client/ |
HTTP/3 client. Uses lsquic (vendored) plus lsqpack. |
src/http/ProxyTunnel.zig |
HTTP CONNECT proxy support. |
src/http/SendFile.zig |
sendfile(2) integration. |
src/http/MimeType.zig, mime_type_list.txt, mime_type_list_enum.zig |
MIME-type table (~256 KB enum). |
src/http/URLPath.zig |
URL path normalisation. |
src/http/websocket.zig, websocket_client.zig, websocket_http_client.zig, websocket_client/ |
WebSocket client (with permessage-deflate). |
Server side
The HTTP server lives in src/bun.js/api/server.zig (~170 KB), the largest file in src/bun.js/api/. It implements Bun.serve({ ... }), including:
- HTTP/1.1, HTTP/2 (with multiplexing), and unencrypted upgrade.
- TLS termination via uSockets and BoringSSL.
- WebSocket upgrade and pubsub broadcast.
- Static file responses (
{ static: { ... } }) — pre-computes the HTTP/2 frame for each entry at startup. - Sub-routing through declarative routes (
{ routes: { "/api/:id": ... } }). - HTML imports — Bake's full-stack mode runs in this server too.
It is paired with C++ host classes in src/bun.js/bindings/ServerBunRequest.cpp, ServerBunResponse.cpp, and the route definitions in src/bun.js/api/server.classes.ts.
Outgoing fetch flow
sequenceDiagram
participant JS as user code
participant Fetch as fetch.zig
participant HTTP as HTTPThread
participant Sock as uSockets
participant Server as remote
JS->>Fetch: fetch(url, opts)
Fetch->>HTTP: enqueue AsyncHTTP request (cross-thread)
HTTP->>Sock: connect
Sock-->>HTTP: writable
HTTP->>Server: request line + headers + body
Server-->>HTTP: response
HTTP->>HTTP: parse via picohttpparser / nghttp2 logic
HTTP-->>Fetch: post Task back to main loop
Fetch-->>JS: Response objectThe HTTP thread is shared by fetch(), bun install (registry calls), bunx, and bun upgrade. There's exactly one HTTPThread per process.
TLS
src/boringssl.zig is a thin wrapper that loads BoringSSL symbols and exposes the API surface uSockets and the HTTP client need. CA bundles ship with Bun (refreshed via .github/workflows/update-root-certs.yml). NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, and SSL_CERT_DIR are honoured.
HTTP/2 and HTTP/3
The HTTP/2 client uses lshpack for HPACK header compression. Frames are parsed in H2FrameParser.zig. The HTTP/3 client uses lsquic for QUIC, lsqpack for QPACK, and the QUIC TLS handshake. Both stacks are statically linked.
The server today serves HTTP/1.1 and HTTP/2 (TLS ALPN-negotiated). HTTP/3 server support is in flight as of 1.3.
WebSocket
The client (src/http/websocket_client.zig, ~75 KB) implements RFC 6455, permessage-deflate (websocket_client/), and a high-level promise-based API exposed as the standard WebSocket global. The server side handles the upgrade in server.zig and delegates frame handling to the same code.
Bun.serve's websocket: { ... } option exposes a pubsub interface with .subscribe()/.publish()/.unsubscribe() per connection, broadcasting via uSockets's underlying us_socket_context_t pubsub primitive.
DNS
src/dns.zig wraps c-ares (vendored at vendor/cares/). All DNS lookups in fetch() and the HTTP thread go through it; the runtime exposes the same as node:dns in src/bun.js/node/dns.zig. The default order is "verbatim" (no IPv4/IPv6 preference); see the BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS env var (default 30s).
Integration points
- uSockets — All sockets, TLS contexts, timers go through
packages/bun-usockets/. - BoringSSL — TLS, hashing, random.
- Event loop — Outgoing requests post results back via
ConcurrentTask. See Event loop. - Server —
Bun.servelives insrc/bun.js/api/server.zig. See Runtime. - Auto cert update —
.github/workflows/update-root-certs.ymlrefreshes the root CA bundle automatically.
Entry points for modification
- To change
fetch()behaviour, look atsrc/bun.js/webcore/fetch.zigfirst; protocol-level changes go inAsyncHTTP.zig. - To change WebSocket framing or pubsub, edit
websocket_client.zigand the server hooks inserver.zig. - To change the server's request lifecycle, edit
src/bun.js/api/server.zig— most code is shaped by theRequestContextlifecycle there. - To change MIME table contents, regenerate
mime_type_list_enum.zigfrommime_type_list.txt.
Key source files
| File | Purpose |
|---|---|
src/http/HTTPThread.zig |
The HTTP thread. |
src/http/AsyncHTTP.zig |
One outgoing request. |
src/bun.js/api/server.zig |
The HTTP server. |
src/http/websocket_client.zig |
WebSocket client / shared frame logic. |
src/boringssl.zig |
TLS bindings. |
src/dns.zig |
c-ares wrapper. |
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.