Static files
Esta página aún no está disponible en tu idioma.
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.
Options
Section titled “Options”| Builder method | Field | Default |
|---|---|---|
withIndexFiles(...n) | indexFiles | ['index.html'] ([] disables) |
withBrowse(flag?) | browse | false |
withCacheControl(v) | cacheControl | header omitted |
withEtag(flag) | etag | true (weak, size+mtime) |
withLastModified(flag) | lastModified | true |
withRanges(flag) | ranges | true |
withDotfiles('deny' | 'allow') | dotfiles | 'deny' |
withSymlinks('within-root' | 'follow') | symlinks | 'within-root' |
withContentTypes(map) | contentTypes | — (ext → type overrides) |
withMaxFileSize(bytes) | maxFileSize | 50 MiB (larger → 413) |
withStreamThreshold(bytes) | streamThreshold | — (unset: nothing streams) |
Behaviour
Section titled “Behaviour”- Conditional requests — a weak
ETag(size + mtime) andLast-Modified; matchingIf-None-Match/If-Modified-Since→304. - Range — a single
bytes=range →206(or416if unsatisfiable);Accept-Ranges: bytesis 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) or404. Underwithin-root, an index file that resolves outside the root counts as absent. - HEAD — full headers, empty body, no file read.
MIME types
Section titled “MIME types”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.
Security model
Section titled “Security model”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
413retires.maxFileSizebounds what is buffered, and a streamed body is not.streamThresholdmust be at or belowmaxFileSize(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-Lengthis 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.servere-frames a stream as chunked,node:httpandDeno.servedo not).
HEAD reads nothing either way.
Where to next
Section titled “Where to next”- Route DSL — how directives compose.
- HTML & XSS — the listing escapes filenames with the same helpers.
- Security — the recommended stack.
