Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
136 commits
Select commit Hold shift + click to select a range
77dd17c
net, tls: consolidate the open node:net, node:tls, Bun.connect, Bun.l…
Jarred-Sumner Oct 22, 2026
a970423
Fix the Windows and macOS test failures, the mordant findings and wha…
Jarred-Sumner Oct 22, 2026
49545fd
http: consolidate the open fetch, Bun.serve, node:http, node:http2 an…
Jarred-Sumner Oct 22, 2026
feebbe0
Fix what review and a differential fuzzer found, the Windows test and…
Jarred-Sumner Oct 22, 2026
8758dd7
Fix what review, a differential fuzzer and the first CI run found
Jarred-Sumner Oct 22, 2026
6d5b579
Fix what the second CI run and review found
Jarred-Sumner Oct 22, 2026
01395ab
Fix what review found in the third push, and the two Windows test rows
Jarred-Sumner Oct 22, 2026
9028a27
node:http2: keep a peer's GOAWAY from crashing a client through a pus…
Jarred-Sumner Oct 22, 2026
c263d3f
node:http: send the head of a HEAD, 204 or 304 response at end() agai…
Jarred-Sumner Oct 22, 2026
76bc84e
node:http, node:http2: fixes from review of the last push
Jarred-Sumner Oct 22, 2026
826df79
fetch, node:http2: take back the per-request cost the release benchma…
Jarred-Sumner Oct 22, 2026
69453d7
Fix the last three differences the differential fuzzer found, and use…
Jarred-Sumner Oct 22, 2026
c2a1d14
fetch: keep FetchTasklet's mutex and atomic flags in one cache line a…
Jarred-Sumner Oct 22, 2026
9337309
Merge branch 'main' into claude/net-tls-consolidated
Jarred-Sumner Oct 22, 2026
67254fe
Merge branch 'main' into claude/http-consolidated
Jarred-Sumner Oct 22, 2026
41b5bfa
Refuse a descriptor this process already uses, and move rules to the …
Jarred-Sumner Oct 22, 2026
9b433a0
Read each TLS server option the way Node does, and move rules to the …
Jarred-Sumner Oct 23, 2026
99da494
Read secure context options by name everywhere, fire socket timeouts …
Jarred-Sumner Oct 23, 2026
c5b696d
Merge branch 'main', give the TLS handshake deadline its own timer, a…
Jarred-Sumner Oct 23, 2026
ae5e404
Fix the lint, clippy and mordant findings, check the descriptor table…
Jarred-Sumner Oct 23, 2026
c7053e2
socket: pass the SSL_CTX to the connect instead of keeping it on the …
Jarred-Sumner Oct 23, 2026
dace09c
Keep the TLS half's queued tail when the raw half of an upgradeTLS() …
Jarred-Sumner Oct 23, 2026
77b8a1c
Merge branch 'main', and close without an error when a timeout handle…
Jarred-Sumner Oct 23, 2026
387f8cc
Keep an error that a TLS callback caught out of its handshake, and re…
Jarred-Sumner Oct 23, 2026
865db48
tls: let the SSL_CTX cache keep the 16 contexts asked for last, so th…
Jarred-Sumner Oct 23, 2026
0ed6d03
test(tls): wait for a Worker's 'close' event before counting the cont…
Jarred-Sumner Oct 23, 2026
851a8c3
socket: close a paused socket whose queued write failed, and report a…
Jarred-Sumner Oct 23, 2026
ebb4da0
Give each HTTP rule one owner: one node:http server per kind of conne…
Jarred-Sumner Oct 23, 2026
a3ae79f
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 23, 2026
525fdb4
Fix the lint, clippy and mordant findings
Jarred-Sumner Oct 23, 2026
a919d6b
Fix what the first CI run on the second pass found, and delete more d…
Jarred-Sumner Oct 23, 2026
e02ba32
Address review comments, and bring in the net / TLS branch's newest s…
Jarred-Sumner Oct 23, 2026
d132a01
node:http: deliver a request body that is read after an await, end a …
Jarred-Sumner Oct 23, 2026
6e8e435
fetch, node:http, Bun.serve: fixes from the final round of verificati…
Jarred-Sumner Oct 23, 2026
5235341
node:http2, node:http, usockets: session.destroy() after close(), unc…
Jarred-Sumner Oct 23, 2026
cf707c7
node:http, node:cluster, child_process, fetch: fixes from the final r…
Jarred-Sumner Oct 23, 2026
eb8280d
node:http: bind the 'finish' callbacks again instead of closing over …
Jarred-Sumner Oct 23, 2026
baf0013
test(node:net, node:http): on Windows a send is never partial, and a …
Jarred-Sumner Oct 23, 2026
a021d40
node:http: honor listen({ ipv6Only: true })
Jarred-Sumner Oct 23, 2026
0676659
Merge branch 'main' into claude/http-consolidated, and make a client …
Jarred-Sumner Oct 24, 2026
c4f8ce9
node:http2: a server session reads as destroyed from the start of its…
Jarred-Sumner Oct 24, 2026
44e4858
node:stream, HTMLRewriter, node:http, node:http2, node:net: fixes fro…
Jarred-Sumner Oct 24, 2026
dee4a75
Merge branch 'main' into claude/http-consolidated
Jarred-Sumner Oct 24, 2026
be1b3a2
usockets, node:child_process: fixes from review; run the Bun.stdin.st…
Jarred-Sumner Oct 24, 2026
04af03e
Merge branch 'main' into claude/http-consolidated
Jarred-Sumner Oct 24, 2026
126551b
node:http2: fixes from review
Jarred-Sumner Oct 24, 2026
9d5ba06
node:http2: a stream that resumes while the session reads gets its WI…
Jarred-Sumner Oct 24, 2026
0b04aed
process.stdin, node:http2: fixes from review
Jarred-Sumner Oct 24, 2026
2962fb4
test(process.stdin): write to the shell pipe from bun, and drain the …
Jarred-Sumner Oct 24, 2026
5f16929
node:http: server.timeout and server.setTimeout() no longer cut a res…
Jarred-Sumner Oct 24, 2026
4f64e4e
node:quic: trailers get their own HEADERS frame, and reach a stream t…
Jarred-Sumner Oct 24, 2026
10a17e1
node:quic: trailers wait for the peer's flow-control credit, like bod…
Jarred-Sumner Oct 24, 2026
bcb2274
node:http, node:net: a socket timeout waits while the peer takes byte…
Jarred-Sumner Oct 24, 2026
02a5482
node:quic: every path that ends a stream's write side drops the heade…
Jarred-Sumner Oct 24, 2026
6f54188
undici: dispatch() refuses a handler without onError, and fetch() wit…
Jarred-Sumner Oct 24, 2026
de337e9
node:net, node:http: a socket's handle has Node's writeQueueSize, and…
Jarred-Sumner Oct 24, 2026
c6596e6
node:quic: destroy() resets a stream whose header block still waits f…
Jarred-Sumner Oct 25, 2026
ec91f3d
node:http: a connection with a held pipelined response is released wh…
Jarred-Sumner Oct 25, 2026
6f66b74
node:http: the socket of an Upgrade request ends when the client leav…
Jarred-Sumner Oct 25, 2026
85750ee
node:http: a keep-alive timeout that finds a write queue still moving…
Jarred-Sumner Oct 25, 2026
fcc81bd
node:http: an 'upgrade' listener that lets go of a request whose body…
Jarred-Sumner Oct 25, 2026
9a3d6c2
node:http: req.socket.bytesWritten only grows, over the responses of …
Jarred-Sumner Oct 25, 2026
7242738
node:net: a write after the peer hung up fails with EPIPE also while …
Jarred-Sumner Oct 25, 2026
07bcf8a
node:http: socket.pause() inside a request listener lasts until the r…
Jarred-Sumner Oct 25, 2026
caf3fe2
node:http: socket.pause() in a 'connection' listener doesn't hold bac…
Jarred-Sumner Oct 25, 2026
3587c38
TLS: end() and shutdown() deliver what was written, also when end() i…
Jarred-Sumner Oct 25, 2026
5015a36
node:http: a request listener's socket.pause() stays when a front ser…
Jarred-Sumner Oct 25, 2026
cd442c7
Bun.serve: a request is released when its client goes away
Jarred-Sumner Oct 25, 2026
451c232
node:https: the listener always gets the server's own TLS context, li…
Jarred-Sumner Oct 25, 2026
9df1656
test(node:net): sockets that start paused leave the event loop idle a…
Jarred-Sumner Oct 25, 2026
ebf6455
test(node:net): a client that fails to connect fails the fixture
Jarred-Sumner Oct 25, 2026
15930f6
Bun.serve: of its two read pauses, only the one for parked requests e…
Jarred-Sumner Oct 25, 2026
77fca77
Bun.serve: requests behind a response leave nothing behind when their…
Jarred-Sumner Oct 25, 2026
4358c86
Bun.serve: a WebSocket that starts after the peer's FIN gets that FIN…
Jarred-Sumner Oct 25, 2026
49621f6
Bun.serve: the mark for requests behind a response counts what waits,…
Jarred-Sumner Oct 25, 2026
3fd0627
test(serve): frame the responses by Content-Length
Jarred-Sumner Oct 25, 2026
9cd5284
process.stdin: pause() without a reader of its own leaves the source'…
Jarred-Sumner Oct 26, 2026
91a69e7
process.stdin: pause() stops a source that no reader holds
Jarred-Sumner Oct 26, 2026
e76056c
node:quic: a write event that gives lsquic a queued header block ends…
Jarred-Sumner Oct 26, 2026
de20f61
process.stdin: pause() and destroy() stop a source that another reade…
Jarred-Sumner Oct 26, 2026
dfde9e6
process.stdin: a reader taken in the tick of destroy() keeps the sour…
Jarred-Sumner Oct 26, 2026
86e2d64
Merge branch 'main' into claude/http-consolidated
Jarred-Sumner Oct 27, 2026
7f35fb6
process.stdin: a destroy() that goes past the instance's own function…
Jarred-Sumner Oct 27, 2026
a9466ca
test(Bun.serve): a server that survives its HTML route's build is rep…
Jarred-Sumner Oct 27, 2026
42ff9ae
test(Bun.serve): say whether a surviving server's cell is in the heap…
Jarred-Sumner Oct 27, 2026
5efd42b
test(Bun.serve): look for a surviving server's address on the native …
Jarred-Sumner Oct 27, 2026
c2088e8
test(Bun.serve): ask the heap what holds the server after its HTML ro…
Jarred-Sumner Oct 27, 2026
a50d48c
Request: the headers have the type of the in-memory body that an unre…
Jarred-Sumner Oct 27, 2026
a145d92
Merge branch 'main' into claude/http-consolidated
Jarred-Sumner Oct 27, 2026
393d0b7
node:quic: read each transport parameter once, and give native code w…
Jarred-Sumner Oct 27, 2026
58ecdf6
test(node:quic): return freed pages to the OS before the outbound que…
Jarred-Sumner Oct 27, 2026
14c718c
test(node:quic): close the client before the assertions, so the serve…
Jarred-Sumner Oct 27, 2026
c762237
Merge branch 'main' into claude/http-consolidated
Jarred-Sumner Oct 28, 2026
2064b74
node:http: compiled servers meet their budgets, 'close' comes in time
Jarred-Sumner Oct 28, 2026
841b431
node:http: requests whose body has ended wait in the internal queue
Jarred-Sumner Oct 28, 2026
1ab1ca5
node:http: a 'data' listener gets the reads after removeAllListeners()
Jarred-Sumner Oct 28, 2026
08f4abd
node:http: take the class below net.Socket out again
Jarred-Sumner Oct 28, 2026
b9a4331
node:http: a body and a FIN that wait in a paused socket are no parse…
Jarred-Sumner Oct 28, 2026
3324f24
Merge branch 'main' into claude/http-consolidated
Jarred-Sumner Oct 28, 2026
fb9678c
Merge branch 'main' into claude/http-consolidated
Jarred-Sumner Oct 28, 2026
66e2bed
test: what keeps a failed stream alive, on macOS too
Jarred-Sumner Oct 28, 2026
8ccb7d6
node:https: https.Server extends tls.Server, as in Node
Jarred-Sumner Oct 29, 2026
b02fb3c
tls: a server does not name the default CA list to its clients
Jarred-Sumner Oct 29, 2026
c74e15b
fetch: ask for a collection for chunked bodies too, and only where it…
Jarred-Sumner Oct 29, 2026
19b5252
trim: the features list C functions that exist
Jarred-Sumner Oct 29, 2026
8a1b1cd
Merge branch 'main' into claude/http-consolidated
Jarred-Sumner Oct 29, 2026
6637934
test: the connect({path}) leak test counts the pages of small blocks
Jarred-Sumner Oct 29, 2026
82bbfbf
aot: the table of built-ins no longer says that new Headers(headers) …
Jarred-Sumner Oct 29, 2026
0566b9e
Merge branch 'main' into claude/http-consolidated
Jarred-Sumner Oct 29, 2026
af6abed
node:http: read no Object.getOwnPropertyDescriptors while the module …
Jarred-Sumner Oct 29, 2026
f955d74
test: the differential test of the built-ins calls the raw setDefault…
Jarred-Sumner Oct 29, 2026
ba1e051
test: the IR test of one name of node:http tells the client by socket…
Jarred-Sumner Oct 29, 2026
f85632d
node: add the two new error codes at the end of the list
Jarred-Sumner Oct 29, 2026
a7c0aef
Merge branch 'main' into claude/http-consolidated
Jarred-Sumner Oct 29, 2026
84b8092
test: request-body-consumers does not depend on when the connection i…
Jarred-Sumner Oct 29, 2026
5e76035
test: the HTTP/2 idle timeout tests wait for the close that is on its…
Jarred-Sumner Oct 29, 2026
eaf99a9
test: write the IR text of custom-accessor-reached-computed-key anew
Jarred-Sumner Oct 29, 2026
1cc8279
test: the HTTP/2 idle timeout tests ask the session whether it is sti…
Jarred-Sumner Oct 29, 2026
2833658
node:quic: a server trusts the list of tls.setDefaultCACertificates()…
Jarred-Sumner Oct 29, 2026
2613145
Merge branch 'main' into claude/http-consolidated
Jarred-Sumner Oct 29, 2026
cfb7494
Merge branch 'main' into claude/http-consolidated
Jarred-Sumner Oct 29, 2026
6837b90
Merge branch 'main' into claude/http-consolidated
Jarred-Sumner Oct 29, 2026
27f3bfc
test: what keeps a failed stream alive runs the test's own program
Jarred-Sumner Oct 29, 2026
001958b
Merge branch 'main' into claude/http-consolidated
Jarred-Sumner Oct 29, 2026
098d7c6
Merge branch 'main' into claude/http-consolidated
Jarred-Sumner Nov 2, 2026
54e6202
bun-types test: fakeLibraryTimers is not published
Jarred-Sumner Nov 2, 2026
88830da
net: the members of the write request get names of their own
Jarred-Sumner Nov 2, 2026
edb9ce8
node-http2-client-close.test.ts: a raw connection starts in a turn of…
Jarred-Sumner Nov 2, 2026
961449c
Merge branch 'main' into claude/http-consolidated
claude Nov 11, 2026
32f5e15
node:http, aot: follow-ups of the merge
claude Nov 11, 2026
e7dda37
Merge branch 'main' into claude/http-consolidated
claude Nov 11, 2026
f418fc9
Format a second text
claude Nov 11, 2026
808d9e8
[autofix.ci] apply automated fixes
autofix-ci[bot] Nov 11, 2026
ee19cba
node:net, node:http, node:dgram: the errors of a failed listen or bin…
claude Nov 11, 2026
44c6245
node:cluster: worker.disconnect() reaches a worker that never loads n…
claude Nov 11, 2026
3f5d7cd
test: say which part of the stand-in test of --isolate fails
claude Nov 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
13 changes: 5 additions & 8 deletions docs/compiler/executables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -834,12 +834,10 @@ import styles from "./styles.css" with { type: "file" };
import { file, serve } from "bun";

serve({
// Inside the compiled executable, file() on an embedded path returns an
// in-memory Blob, which routes only accepts wrapped in a Response.
routes: {
"/favicon.ico": new Response(file(favicon)),
"/logo.png": new Response(file(logo)),
"/styles.css": new Response(file(styles)),
"/favicon.ico": file(favicon),
"/logo.png": file(logo),
"/styles.css": file(styles),
},
fetch(req) {
return new Response("Not found", { status: 404 });
Expand Down Expand Up @@ -1036,12 +1034,11 @@ import "./public/styles.css" with { type: "file" };
import { embeddedFiles, serve } from "bun";

// Build static routes from all embedded files
const staticRoutes: Record<string, Response> = {};
const staticRoutes: Record<string, Blob> = {};
for (const blob of embeddedFiles) {
// Remove hash from filename: "icon-a1b2c3d4.png" -> "icon.png"
const name = blob.name.replace(/-[a-z0-9]+\./, ".");
// embeddedFiles are plain Blobs, which routes does not accept directly
staticRoutes[`/${name}`] = new Response(blob);
staticRoutes[`/${name}`] = blob;
}

serve({
Expand Down
10 changes: 5 additions & 5 deletions docs/guides/http/fetch-unix.mdx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
title: fetch with unix domain sockets in Bun
sidebarTitle: Fetch with unix domain sockets
title: fetch with Unix domain sockets in Bun
sidebarTitle: Fetch with Unix domain sockets
mode: center
---

In Bun, `fetch()` can send HTTP requests over a [unix domain socket](https://en.wikipedia.org/wiki/Unix_domain_socket) with the `unix` option.
In Bun, `fetch()` can send HTTP requests over a [Unix domain socket](https://en.wikipedia.org/wiki/Unix_domain_socket) with the `unix` option.

```ts fetch-unix.ts icon="/icons/typescript.svg"
const unix = "/var/run/docker.sock";
Expand All @@ -17,9 +17,9 @@ console.log(body); // { ... }

---

The `unix` option is the local file path to a unix domain socket. `fetch()` sends the request over that socket instead of a TCP connection. HTTPS is also supported: use the `https://` protocol in the URL instead of `http://`.
The `unix` option is the local file path to a Unix domain socket. `fetch()` sends the request over that socket instead of a TCP connection. HTTPS is also supported: use the `https://` protocol in the URL instead of `http://`.

To send a `POST` request to an API endpoint over a unix domain socket:
To send a `POST` request to an API endpoint over a Unix domain socket:

```ts fetch-unix.ts icon="/icons/typescript.svg"
const response = await fetch("https://hostname/a/path", {
Expand Down
17 changes: 15 additions & 2 deletions docs/guides/http/stream-file.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,30 @@ new Response(Bun.file("./img.png")).headers.get("Content-Type");

Putting it all together with [`Bun.serve()`](/runtime/http/server).

The client chooses the request path, and `new URL(req.url).pathname` starts with `/`. Passing it straight to `Bun.file()` would serve any file the process can read. Resolve it against a fixed directory and reject anything outside that directory.

```ts server.ts icon="/icons/typescript.svg"
import path from "node:path";

const root = path.resolve("./public");

// static file server
Bun.serve({
async fetch(req) {
const path = new URL(req.url).pathname;
const file = Bun.file(path);
const filePath = path.join(root, decodeURIComponent(new URL(req.url).pathname));
if (!filePath.startsWith(root + path.sep)) return new Response("Forbidden", { status: 403 });
Comment thread
Jarred-Sumner marked this conversation as resolved.
Resolved

const file = Bun.file(filePath);
if (!(await file.exists())) return new Response("Not Found", { status: 404 });
return new Response(file);
},
});
```

The check compares paths, so a symbolic link inside `public` is still followed to wherever it points. Don't keep links to private files there.

To serve a whole directory without writing this yourself, use a [directory route](/runtime/http/routing#directory-routes).

---

See [`Bun.write()`](/runtime/file-io#writing-files-bun-write).
4 changes: 3 additions & 1 deletion docs/runtime/debugger.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ await fetch("https://example.com", {
```

```txt
[fetch] $ curl --http1.1 "https://example.com/" -X POST -H "content-type: application/json" -H "Connection: keep-alive" -H "User-Agent: Bun/1.3.3" -H "Accept: */*" -H "Host: example.com" -H "Accept-Encoding: gzip, deflate, br" --compressed -H "Content-Length: 13" --data-raw "{\"foo\":\"bar\"}"
[fetch] $ curl --http1.1 'https://example.com/' -X POST -H 'content-type: application/json' -H 'Connection: keep-alive' -H 'User-Agent: Bun/1.3.3' -H 'Accept: */*' -H 'Host: example.com' -H 'Accept-Encoding: gzip, deflate, br' --compressed -H 'Content-Length: 13' --data-raw '{"foo":"bar"}'
[fetch] > HTTP/1.1 POST https://example.com/
[fetch] > content-type: application/json
[fetch] > Connection: keep-alive
Expand All @@ -169,6 +169,8 @@ await fetch("https://example.com", {

The lines with `[fetch] >` are the request from your local code, and the lines with `[fetch] <` are the response from the remote server.

On macOS and Linux, each argument of the `curl` command is single-quoted, so a shell like `bash` or `zsh` passes `$`, backticks and double quotes to `curl` as they are. On Windows, the arguments are double-quoted for `cmd.exe`.

To print without the `curl` command, set `BUN_CONFIG_VERBOSE_FETCH` to `true`.

```ts index.ts icon="/icons/typescript.svg"
Expand Down
1 change: 1 addition & 0 deletions docs/runtime/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ Bun reads these environment variables to configure aspects of its behavior.
| `NO_COLOR` | If `NO_COLOR=1`, then ANSI color output is [disabled](https://no-color.org/). |
| `FORCE_COLOR` | If `FORCE_COLOR=1`, then ANSI color output is forced on, even if `NO_COLOR` is set. |
| `BUN_CONFIG_MAX_HTTP_REQUESTS` | Sets the maximum number of concurrent HTTP requests sent by `fetch` and `bun install`. `fetch` defaults to `256`. `bun install` defaults to `64`, and its `--network-concurrency` flag overrides this variable. Lower it if you run into rate limits or connection issues. |
| `BUN_CONFIG_HTTP_IDLE_TIMEOUT` | Seconds an HTTP request sent by `fetch` or `bun install` can go without progress before it fails with a timeout error. The timer covers the TLS handshake, the response headers and the response body. Defaults to `300`. `0` disables it. |
| `BUN_INSTALL_WINDOWS_RENAME_RETRY_MS` | Windows only. How many milliseconds `bun install` keeps retrying a rename that fails because another process, usually antivirus, has a file in the directory open. Defaults to `5000`. |
| `BUN_CONFIG_WS_CLOSE_TIMEOUT` | Sets how many seconds a `WebSocket` client waits after sending a Close frame for the server to reply and close the connection. After that, Bun closes it. If the server never sent a Close frame, the `close` event has code `1006`. Defaults to `30`. `0` disables the timeout. |
| `BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD` | If `BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD=true`, then `bun --watch` does not clear the console on reload |
Expand Down
9 changes: 8 additions & 1 deletion docs/runtime/http/error-handling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,20 @@ In development mode, when a request handler throws and no `error` handler return

To handle server-side errors, implement an `error` handler. Return a `Response` to serve to the client when an error occurs. In `development` mode, this response replaces Bun's default error page.

Error messages and stack traces can contain request input and absolute file paths. Escape them with [`Bun.escapeHTML()`](/runtime/utils#bun-escapehtml) before you put them in HTML, and don't send them to clients in production.

```ts
Bun.serve({
fetch(req) {
throw new Error("woops!");
},
error(error) {
return new Response(`<pre>${error}\n${error.stack}</pre>`, {
console.error(error);
if (process.env.NODE_ENV === "production") {
return new Response("Internal Server Error", { status: 500 });
}
return new Response(`<pre>${Bun.escapeHTML(`${error}\n${error.stack}`)}</pre>`, {
status: 500,
headers: {
"Content-Type": "text/html",
},
Expand Down
26 changes: 18 additions & 8 deletions docs/runtime/http/routing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ Bun.serve({
});
```

Bun automatically decodes percent-encoded route parameter values, including Unicode characters. Bun replaces invalid Unicode with the Unicode replacement character (`\uFFFD`).
A path segment is a parameter only when it starts with `:`, and the parameter captures the whole segment. A `:` anywhere else in a segment is matched literally, so `/files:batchGet/:id` has one parameter, `id`.

Bun automatically decodes percent-encoded route parameter values, including Unicode characters. Bun replaces invalid Unicode with the Unicode replacement character (`\uFFFD`). A `%` that isn't followed by two hexadecimal digits stays a literal `%`.

### Static responses

Expand Down Expand Up @@ -163,10 +165,14 @@ Bun caches static route responses for the lifetime of the server object. To relo
Serving a file from a route behaves differently depending on whether you buffer the file content or serve it directly:

```ts
const logo = Bun.file("./logo.png");

Bun.serve({
routes: {
// Static route - content is buffered in memory at startup
"/logo.png": new Response(await Bun.file("./logo.png").bytes()),
"/logo.png": new Response(await logo.bytes(), {
headers: { "Content-Type": logo.type },
}),

// File route - content is read from filesystem on each request
"/download.zip": new Response(Bun.file("./download.zip")),
Expand All @@ -181,6 +187,7 @@ Bun.serve({
- **If-None-Match** - Returns `304 Not Modified` when client ETag matches
- **No 404 handling** - Missing files cause startup errors, not runtime 404s
- **Memory usage** - Full file content stored in RAM
- **No Content-Type** - Bytes have no type, so set the `Content-Type` header yourself. `file.type` is the type Bun infers from the file extension
- **Best for**: Small static assets, API responses, frequently accessed files

**File routes** (`new Response(Bun.file(path))`) read from filesystem per request:
Expand All @@ -207,7 +214,7 @@ Bun.serve({
});
```

Bun percent-decodes the part of the request URL after the prefix once and opens it relative to `dir`. Bun rejects non-canonical paths with `404`, so the served path is always the path the router matched. A path is non-canonical if it contains `.`, `..`, empty segments, `%2F`, or a `%XX` sequence encoding a character that may appear literally in a path segment.
Bun percent-decodes the part of the request URL after the prefix once and opens it relative to `dir`. Bun rejects non-canonical paths with `404`, so the served path is always the path the router matched. A path is non-canonical if it contains a segment that is exactly `.` or `..`, an empty segment, `%2F`, or a `%XX` sequence encoding a character that may appear literally in a path segment.

A symlink never leads out of `dir`. Bun resolves symlinks as if `dir` were the root directory:

Expand All @@ -216,6 +223,8 @@ A symlink never leads out of `dir`. Bun resolves symlinks as if `dir` were the r

These are the rules of `openat2(RESOLVE_IN_ROOT)` on Linux. Bun applies them itself on macOS, on FreeBSD, and on Linux when `openat2` is not available. On Windows, Bun follows symlinks wherever they lead.

Bun doesn't filter dotfiles. It serves any regular file under `dir`, including a `.env` file or the contents of a `.git` directory. Point `dir` at a directory that only contains public files.

<Info>
Routing is case-sensitive, but filesystems on macOS and Windows are case-insensitive by default. As a result, a
case-varied URL (`/static/Admin/secret.txt`) routes to the directory wildcard rather than a sibling `/static/admin/*`
Expand Down Expand Up @@ -259,15 +268,16 @@ Bun.serve({
fetch(req) {
// parse `Range` header
const [start = 0, end = Infinity] = req.headers
.get("Range") // Range: bytes=0-100
.split("=") // ["Range: bytes", "0-100"]
.get("Range") // "bytes=0-100"
.split("=") // ["bytes", "0-100"]
.at(-1) // "0-100"
.split("-") // ["0", "100"]
.map(Number); // [0, 100]
.split("-") // ["0", "100"], or ["100", ""] for "bytes=100-"
.map(part => (part ? Number(part) : undefined)); // [0, 100]

// return a slice of the file
// the end of a `Range` is inclusive and the end of a `slice` is exclusive, so add 1
const bigFile = Bun.file("./big-video.mp4");
return new Response(bigFile.slice(start, end));
return new Response(bigFile.slice(start, end + 1));
},
});
```
Expand Down
58 changes: 52 additions & 6 deletions docs/runtime/http/server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,48 @@ const server = Bun.serve({
console.log(`Server running at ${server.url}`);
```

### Splitting routes across files

`routes` is a plain object, so you can define groups of routes in separate files and spread them together. Annotate `req` with `BunRequest<path>` to keep `req.params` typed.

```ts title="users.ts" icon="/icons/typescript.svg"
import type { BunRequest } from "bun";

export const userRoutes = {
"/users/:id": (req: BunRequest<"/users/:id">) => {
return new Response(`Hello User ${req.params.id}!`);
},
};
```

```ts title="posts.ts" icon="/icons/typescript.svg"
export const postRoutes = {
"/api/posts": {
GET: () => new Response("List posts"),
POST: async (req: Request) => {
const post = await req.json();
return Response.json({ created: true, post });
},
},
};
```

```ts title="index.ts" icon="/icons/typescript.svg"
import { userRoutes } from "./users";
import { postRoutes } from "./posts";

const server = Bun.serve({
routes: {
...userRoutes,
...postRoutes,
},

fetch(req) {
return new Response("Not Found", { status: 404 });
},
});
```

---

## HTML imports
Expand Down Expand Up @@ -147,7 +189,7 @@ NODE_PORT=4002 bun server.ts

## Unix domain sockets

To listen on a [unix domain socket](https://en.wikipedia.org/wiki/Unix_domain_socket), pass the `unix` option with the path to the socket.
To listen on a [Unix domain socket](https://en.wikipedia.org/wiki/Unix_domain_socket), pass the `unix` option with the path to the socket.

```ts
Bun.serve({
Expand All @@ -171,7 +213,7 @@ Bun.serve({
});
```

Unlike unix domain sockets, abstract namespace sockets are not bound to the filesystem and are automatically removed when the last reference to the socket is closed.
Unlike Unix domain sockets, abstract namespace sockets are not bound to the filesystem and are automatically removed when the last reference to the socket is closed.

---

Expand Down Expand Up @@ -213,7 +255,7 @@ Bun.serve({
```

<Note>
`http3` is not supported with unix domain sockets: QUIC requires a UDP port. `http1: false` requires `http2: true` or
`http3` is not supported with Unix domain sockets: QUIC requires a UDP port. `http1: false` requires `http2: true` or
`http3: true`.
</Note>

Expand Down Expand Up @@ -252,7 +294,7 @@ Set `http1: false` to refuse HTTP/1.x clients. Over TLS, clients that offer ALPN

By default, `Bun.serve` closes connections after **10 seconds** of inactivity. A connection is idle when no data is being sent or received. That includes in-flight requests where your handler is still running but hasn't written any bytes to the response yet. Browsers and `fetch()` clients see this as a connection reset.

To configure this, set the `idleTimeout` field (in seconds). The maximum value is `255`, and `0` disables the timeout entirely.
To configure this, set the `idleTimeout` field (in seconds). The maximum value is `255`, and `0` disables the timeout entirely. Bun checks for idle connections every 4 seconds, so a connection is closed up to 4 seconds after its `idleTimeout` has passed, and never before.

```ts
Bun.serve({
Expand All @@ -265,6 +307,8 @@ Bun.serve({
});
```

`idleTimeout` is also how long Bun keeps an idle keep-alive connection open between requests. Behind a load balancer or a reverse proxy, set it above the keep-alive time the proxy uses for its upstream connections, or to `0`. Otherwise the proxy can send a request on a connection that Bun is closing.

<Note>
**Streaming & Server-Sent Events** — The idle timer applies while a response is being streamed. If your stream goes
quiet for longer than `idleTimeout`, Bun closes the connection mid-response. For long-lived streams, disable the
Expand Down Expand Up @@ -336,6 +380,8 @@ await server.stop(true);

By default, `stop()` allows in-flight requests and WebSocket connections to complete. The server closes idle keep-alive connections immediately. Connections with a request in flight close once the server has sent their response. Pass `true` to immediately terminate all connections instead. Open WebSockets close with code `1001`. The returned promise resolves once every connection has closed.

The last HTTP/1.1 response on a connection carries `Connection: close`. An HTTP/2 connection gets a `GOAWAY` frame, and once its streams are done the server ends its side of the connection. The connection closes when the client ends its side too, or after 10 seconds.

### `server.closeIdleConnections()`

To close keep-alive connections that are not currently serving a request, without stopping the server:
Expand Down Expand Up @@ -392,7 +438,7 @@ Use this for development and hot reloading. You can update only `fetch`, `error`

### `server.timeout(Request, seconds)`

Override the idle timeout for an individual request. Pass `0` to disable the timeout entirely for that request.
Override the idle timeout for an individual request. Pass `0` to disable the timeout entirely for that request. The maximum is `255`, and larger values are clamped to it.

```ts
const server = Bun.serve({
Expand Down Expand Up @@ -621,7 +667,7 @@ interface Server extends Disposable {
* Make a request to the running server.
* Useful for testing or internal routing.
*/
fetch(request: Request | string): Response | Promise<Response>;
fetch(request: Request | string | URL, init?: RequestInit): Response | Promise<Response>;

/**
* Upgrade an HTTP request to a WebSocket connection.
Expand Down
16 changes: 16 additions & 0 deletions docs/runtime/http/tls.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ Bun.serve({
});
```

### Protocol versions

To limit the TLS protocol versions a connection can use, set `minVersion` and `maxVersion`. They take the same names as `node:tls`: `"TLSv1"`, `"TLSv1.1"`, `"TLSv1.2"` and `"TLSv1.3"`. The defaults are `"TLSv1.2"` and `"TLSv1.3"`.

```ts
Bun.serve({
tls: {
key: Bun.file("./key.pem"),
cert: Bun.file("./cert.pem"),
minVersion: "TLSv1.3", // [!code ++]
},
});
```

The same two options work in every API that takes a `tls` object, such as `fetch`, `Bun.connect` and `Bun.listen`. When `tls` is an array (see [SNI](#server-name-indication-sni) below), every entry uses the range of the first one.

### CA Certificates

Pass `ca` to override the trusted CA certificates. By default, the server trusts the list of well-known CAs curated by Mozilla; setting `ca` replaces that list.
Expand Down
Loading
Loading