Перейти к содержимому
Русский

Static files

Это содержимое пока не доступно на вашем языке.

Serve files without reaching for a backend-specific plugin. getFromFile serves one file; getFromDirectory maps a URL onto a directory tree; getFromBrowseableDirectory adds HTML listings.

import { concat, getFromFile, getFromBrowseableDirectory, path } from 'actor-ts/http';
const routes = concat(
path('logo.svg', getFromFile('./assets/logo.svg')),
getFromBrowseableDirectory('static', './public'), // mounts at /static
);

getFromDirectory([routePrefix,] fsRoot, options?) — with a routePrefix it mounts under it (e.g. /static/...), without one it mounts at the root.

Builder methodFieldDefault
withIndexFiles(...n)indexFiles['index.html'] ([] disables)
withBrowse(flag?)browsefalse
withCacheControl(v)cacheControlheader omitted
withEtag(flag)etagtrue (weak, size+mtime)
withLastModified(flag)lastModifiedtrue
withRanges(flag)rangestrue
withDotfiles('deny' | 'allow')dotfiles'deny'
withSymlinks('within-root' | 'follow')symlinks'within-root'
withContentTypes(map)contentTypes— (ext → type overrides)
withMaxFileSize(bytes)maxFileSize50 MiB (larger → 413)
withStreamThreshold(bytes)streamThreshold— (unset: nothing streams)
  • Conditional requests — a weak ETag (size + mtime) and Last-Modified; matching If-None-Match / If-Modified-Since → 304.
  • Range — a single bytes= range → 206 (or 416 if unsatisfiable); Accept-Ranges: bytes is advertised. Only the requested window is read, never the whole file.
  • Directories — a request without a trailing slash 301-redirects to add it (query preserved); with a slash, index files are tried in order, then a listing (if browsing) or 404. Under within-root, an index file that resolves outside the root counts as absent.
  • HEAD — full headers, empty body, no file read.

Content-types come from contentTypeFor(pathOrExt, overrides?) — a small registry of ~45 common web types. Text-ish types get ; charset=utf-8; unknown extensions fall back to application/octet-stream. Override per-route with withContentTypes({ ext: 'type' }) or call contentTypeFor directly when building responses by hand.

The URL remainder is fully decoded before validation (peeling every encoding layer), then every path segment is rejected if it is .., empty, a NUL, a backslash, or a : segment (Windows drive / NTFS alternate data stream); absolute forms are refused, the joined path is confined to the root, and a symlink escaping the root is refused (within-root default). Dotfiles are denied by default. Every rejection is a uniform 404 — no information leak about what exists.

Confinement is checked on every filesystem hop a request touches, not only on the path the URL resolved to: a directory’s index file and each entry of a browsable listing are canonicalised as well. A link that escapes the root is a 404 whether it is asked for directly or reached as index.html, and it is left out of the listing entirely — its name, size and mtime are exactly what the policy withholds. Switch to withSymlinks('follow') when the tree links outside itself on purpose (a build output, a package-manager link farm).

Memory: buffered by default, streamed on request

Section titled “Memory: buffered by default, streamed on request”

A body is read into memory by default, bounded by maxFileSize (50 MiB → a larger file gets 413). A Range reads only the bytes it asked for, so a bytes=0-0 on a 50 MiB file costs one byte, not 50 MiB.

Set streamThreshold and a body at or above it is sent as a stream, read 64 KiB at a time — so serving a file larger than the process’s memory costs the same as serving a small one:

import { getFromDirectory, StaticFilesOptions } from 'actor-ts/http';
const largeDownloads = StaticFilesOptions.create()
.withStreamThreshold(8 * 1024 * 1024)
.withMaxFileSize(8 * 1024 * 1024);
const routes = getFromDirectory('downloads', '/srv/releases', largeDownloads);

Two things follow from setting it:

  • The 413 retires. maxFileSize bounds what is buffered, and a streamed body is not. streamThreshold must be at or below maxFileSize (a higher one is rejected as a contradiction), which means nothing can buffer past the threshold — the cap becomes unreachable rather than waived, so no bound is lost.
  • Content-Length is stated for you. A backend has nothing to measure on a stream, so the handler sets the header itself. Fastify and Express put it on the wire; on Hono it depends on the runtime’s server (Bun.serve re-frames a stream as chunked, node:http and Deno.serve do not).

HEAD reads nothing either way.

  • Route DSL — how directives compose.
  • HTML & XSS — the listing escapes filenames with the same helpers.
  • Security — the recommended stack.