Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions docs/1.guide/4.middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,28 @@ When no file matches the request, it calls `next()` — so your handler acts as

- `dir`: The directory to serve files from (required).
- `methods`: HTTP methods to serve (default `["GET", "HEAD"]`). Other methods fall through to `next()`.
- `renderHTML`: A function receiving `{ request, html, filename }` for every `.html` file, returning the `Response` to send. Use it to inject or template markup before serving.
- `dotfiles`: Dot segments (path segments starting with `.`) that may be served (default `[".well-known"]`). A path containing any other dot segment — `/.env`, `/.git/config` — falls through to `next()`. Pass `true` to serve every dot segment, or `false` (or `[]`) to serve none, including `/.well-known/`.
- `encodings`: Serve precompressed variants from disk (default `false`). Pass `true` for `{ br: ".br", gzip: ".gz" }`, or a map setting the extension per encoding (keys tried in order, preferred first). Off by default because most deployments ship no precompressed files, so the lookup is a `stat` that always misses.
- `compress`: Compress a response on the fly when no precompressed variant is served (default `true`). Pass `false` to serve only what is already on disk.
- `renderHTML`: A function receiving `{ request, html, filename }` for every HTML file (`.html`, `.htm`), returning the `Response` to send. Use it to inject or template markup before serving.

A request resolves in order: the path itself, then `<path>.html`, then `<path>/index.html`. So `/about` serves `about.html`, while an extension-less file (`LICENSE`, an ACME challenge token) is served at its exact name. A trailing-slash request names a directory, so `/sub/` resolves only `sub/index.html` — never `sub.html` or a file named `sub`.

By default a compressible response is compressed on the fly as it is sent. Enabling `encodings` adds a disk lookup that takes precedence: for `/app.js` with `Accept-Encoding: br`, `app.js.br` is served if it exists (with `Content-Encoding: br`), and only a missing variant falls back to on-the-fly. A variant always wins because it costs no CPU, and a build can afford a better ratio than a per-request encode can justify — so `encodings: true` plus a build step is the cheapest way to serve maximum-quality compressed assets. The two switches are independent: `compress: false` serves only what is on disk, and `encodings` off with `compress` on always compresses on the fly.

Brotli compresses at quality 4 rather than the `node:zlib` default of 11, which costs roughly 12x the CPU for a few percent of size. Only files between 1 KiB and 10 MiB are compressed on the fly: below that the encoded body can come out larger than the input, and above it the CPU spent per request outweighs the bandwidth saved — precompress large assets instead. On-the-fly responses are chunked, since the encoded length is not known until the bytes exist, while a variant declares the `Content-Length` it has on disk.

Compression applies to compressible types only, so a `.br` next to an image or font is ignored, and those responses omit `Vary: Accept-Encoding` — which compressible ones always set, including uncompressed ones, as a shared cache must key on the header either way. `renderHTML` routes are never compressed and always read the source file: a variant on disk would not match the rendered output, and the `Response` the hook returns is the caller's to encode.

`/.well-known/` is served by default because [RFC 8615](https://www.rfc-editor.org/rfc/rfc8615) reserves it for public metadata: ACME HTTP-01 challenges and `security.txt` live there. Allow-listing is by exact segment name, so `[".well-known"]` serves neither a sibling sharing its prefix (`.well-known-backup`) nor a dot segment nested under it (`.well-known/.env`).

Text responses declare `charset=utf-8`; without it a browser decodes them with a fallback of its own choosing, mangling any non-ASCII byte the file does not declare inline.

> [!NOTE]
> Files are only served from within `dir`, and both rules above are re-checked against the path a symlink actually resolves to. Symlinks are followed, but one resolving outside `dir` — or onto a dot segment `dotfiles` hides, such as `public.txt` → `.env` — falls through to `next()` instead of being served.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
> [!NOTE]
> Despite the runtime-neutral name, `srvx/static` is **Node-API-only** — it uses `node:fs` and `node:zlib` internally, so it works only on runtimes with Node.js compatibility (Node, Deno, Bun).
> Despite the runtime-neutral name, `srvx/static` is **Node-API-only** — it uses `node:fs` internally, so it works only on runtimes with Node.js compatibility (Node, Deno, Bun).

See [Serving static files](/guide/cli#serving-static-files) for the equivalent CLI flag.

Expand Down
Loading
Loading