Expand description
§mini-static
A secure static file server: HTTP/1.1, streaming responses, traversal-safe path resolution, hidden files denied by default, live reload, precompressed sidecars, external-tool CSS/JS bundling and minification, directory-index redirects. No templating, no framework — files in, HTTP responses out.
Status: published, actively developed. See
PLAN.mdfor the roadmap.
[dependencies]
mini-static = "0.28"§Design
Server::new(root) canonicalizes root once at startup and serves everything
below it. Every resolved path is checked against the canonicalized root before any file
I/O happens — the canonicalization is the security boundary, not a pattern match on ...
Source folders and an output dir: optional source folders (with_source_folder)
hold the inputs to the build pipelines — CSS that feeds a single bundle, JS that is
minified per-file. A single output dir (with_output_dir, defaulting to the served
root) receives the processed outputs. The two are disjoint by construction: a source
folder that overlaps the output dir is rejected, and the output dir is never a watcher
trigger — pipelines react to source folders only, so a pipeline’s own output can never
re-trigger its own rebuild.
§Source-folder build pipelines
The server can build its own static output from with_source_folder(path) trees. When
any source folder or a CSS/JS tool is configured, a startup build runs full_build;
with with_live_reload() the source folders are watched and each change re-runs the
appropriate step. Everything lives under the one output dir (with_output_dir, default
the served root):
- CSS and JS are each independently opt-in, via
with_css_tool(tool, options)/with_js_tool(tool, options). With neither configured, source folders are watched (for live-reload) but nothing is transformed or copied to the output dir. bundle/minifyare independent toggles per language (CssOptions/JsOptions), all four combinations valid: passthrough copy, per-file minify (no@import/module resolution), bundle only (unminified, useful for debugging), or bundle + minify.- A source change re-runs only the affected step — a bundle-mode edit rebuilds the whole bundle, a per-file-mode edit rebuilds just that file — and then the reload broadcast is emitted after the output is written, so the browser reloads content that already exists.
- Prune is opt-in and build-time only.
with_prune_output()deletes the CSS bundle at startup when no CSS sources remain; it never runs during live-reload. - Asset folders (
with_asset_folder(path)) are a third, simpler source kind. Every file under one — any extension,index.html, images, whatever — is mirrored byte-identical into the output dir, preserving its path relative to the asset folder. No CSS/JS tool involved, just a flat copy on startup and on every live-reload change. Use this for hand-authored static files that should live outside the served/output dir as source, the same separation the CSS/JS pipelines already have. Rejected if it overlaps the output dir or another registered source/asset folder, for the same feedback-loop reasonwith_source_folderis. Server::build()runs every configured pipeline once and returns, no HTTP server involved. For deploy tooling that wants the output dir populated ahead of time — e.g. a one-shotcargo run --bin build_staticstep before baking a Docker image — mirroring a one-shot content builder’sbuild()(such asmini_docs::Builder::build()) rather than needing to start-and-kill a live server just to get one build out of it.
§Protocol surface
HTTP/1.1 only, deliberately. Through 0.21.x the connection was served by
hyper_util’s auto builder, whose server-auto feature transitively enables
hyper/http2 — so a prior-knowledge h2c client negotiated HTTP/2 against a server
that documented, tested, and tuned only HTTP/1: no stream-concurrency limit, no
frame-size bound, and a header pre-read whose \r\n\r\n scan the HTTP/2 preface
satisfies without being an HTTP/1 request at all. 0.22.0 pins the surface to
hyper::server::conn::http1 and drops the h2 dependency entirely. Browsers reach
HTTP/2 over TLS only, which this crate does not terminate — put a reverse proxy in
front for h2/h3.
§Connection lifecycle (fixed from the prior iteration)
- Header-read timeout, per request. Every request has a bounded time (default 30s)
to send its headers before the connection is dropped, and a 64 KiB ceiling on the
header block. The original implementation had no timeout at all: a client that opened
a socket and sent nothing held a connection-semaphore permit forever — 1024 idle
sockets (the default
max_connections), trivially cheap for an attacker, permanently stopped the server from accepting anyone else. The fix for that enforced both bounds in a hand-rolled pre-read that ran once per connection, which left the same hole one step further in: a client could complete one cheap request and then stall mid-header forever on the same keep-alive connection, bounded by nothing. 0.23.0 delegates both bounds to hyper (header_read_timeout,max_buf_size), which applies them to every request on a connection, and deletes ~100 lines of socket plumbing. Response bodies are deliberately unbounded by this timeout — the live-reload SSE stream stays open until a watched file changes. - Ephemeral binds are loopback-only (
127.0.0.1:0), matchingmini-serve’s fix and for the same reason: a test helper should never expose a real file server to the LAN. - A transient
accept()error no longer ends the server. A sustained failure (e.g. the process is out of file descriptors) now degrades into periodic retries with exponential backoff instead of a single error silently ending the accept loop for good, or a naive retry busy-spinning at 100% CPU. Mirrorsmini-serve’sBackoff. run()/run_ephemeral()return aServerHandlealongside the port. The prior implementation had no way to stop a running server short of exiting the process — every test that started one leaked its background accept loop for the rest of the test binary’s life, and an embedder had no way to stop serving at all. Callinghandle.shutdown().awaitstops accepting new connections and waits for already-accepted connections to finish before returning; dropping the handle without calling it preserves the old fire-and-forget behavior.
§Path traversal responses (fixed)
A blocked traversal attempt and a genuinely missing file both answer 404 not found.
The prior implementation answered traversal attempts with a distinct 403 and a
distinctive message — telling a prober exactly when they’d found the guard, and
inviting iteration to map the filesystem by response code. The two responses are now
byte-identical, so the distinction is not observable over the wire at all.
Every response carries X-Content-Type-Options: nosniff — the server serves
user-supplied directories, and content-sniffing a mislabeled file is a real vector for
stored XSS. That includes 304s as of 0.24.1; before then the revalidation path built
its own response and was the one status that could arrive without the header, which is
backwards — a revalidating client is precisely the one holding the cached copy.
§Hidden files
Dot-prefixed paths are denied by default (0.24.0). A request for /.env or
/.git/config answers 404, byte-identical to a miss — an existing dotfile and a
missing one are indistinguishable, for the same reason a traversal and a miss are. The
traversal guard cannot help here: those files are legitimately inside the root, so
before this any visitor who guessed the name got them, and a served root is routinely a
build output directory or a repository working copy.
/.well-known/ is served regardless — it is where the web puts resources meant to be
fetched (ACME challenges for certificate issuance, security.txt), and denying it would
break certificate renewal. The exception covers the first segment only:
/.well-known/.hidden is still denied. A segment of exactly . is a same-directory
reference, not a hidden name, so /./index.html still serves.
The check runs on the decoded request path, so %2E cannot smuggle a dot past it,
and never on the served root’s own filesystem path — a root that itself lives under a
dot-directory (~/.config/site/public) keeps working. Server::with_hidden_files()
restores the old behavior for roots whose dotfiles are genuinely content.
The traversal pre-check matches path segments equal to .., not any substring
containing .. — the prior substring check rejected legitimate filenames like
jquery..min.js. canonicalize() + starts_with(root) remains the actual security
boundary; the segment check is a cheap early rejection, not the guarantee.
§HTTP correctness (fixed)
- Method handling. Only
GET/HEADserve files; everything else gets405withAllow: GET, HEAD. The prior implementation served files for any method, including streaming the full body for HEAD requests before hyper silently dropped it on the wire — real disk I/O for a response nobody could see. Content-Lengthon streamed responses. The file’s length is known before streaming begins (metadata.len()) and is now sent on every full-file200, not only on range responses — without it, clients fall back to chunked encoding and lose progress bars and cacheability by size.- Directory index redirects. Requesting
/dirwhen/dir/index.htmlexists issues a301to/dir/first, so relative links inside the served page resolve against the right base — the prior implementation served the index directly at/dir, silently breaking every relative link on the page. This is deliberately not configurable./dir/is the canonical URL of a directory index, and a browser resolves a page’s relative paths against the last/in the address: the sameimg.pngmeans/dir/img.pngat/dir/and/img.pngat/dir. Serving both would give one page two addresses, one of which quietly resolves its own assets to the wrong place. Slashless URLs are a coherent choice under a different mechanism — servingdir.htmlat/dir, the way Vercel and Netlify hide extensions — where the resource is a file and no directory base exists to get wrong. 0.20.0 briefly offered aTrailingSlash::Serveoption that mixed the two; it was removed in 0.21.0. - Custom 404 page.
Server::with_not_found_page(Path::new("404.html"))serves that file as the body of every miss, keeping the404status (a200would be a soft 404 — indexed by search engines, invisible to monitoring) and addingCache-Control: no-store. Opt-in: a404.htmlsitting in the root does nothing on its own. The path is validated when configured, so a missing page fails at startup rather than on the first broken link, and it is read per response so an edit lands without a restart. Nothing about the failed request reaches the page — a miss and a rejected traversal return identical bytes, preserving thenot foundcollapse inStaticError::user_message. - Range requests. Single-range requests (e.g.,
bytes=0-99) get a206 Partial Contentresponse with the requested byte range. Multi-range requests (e.g.,bytes=0-99,200-299) are treated as invalid and return a full200with the whole body (RFC 9110-legal, matches common server behavior). Out-of-bounds ranges return416 Range Not Satisfiable.If-Rangevalidation: exact strong ETag match only; staleIf-Rangecauses a full200response. Precompressed sidecars are skipped for range requests (the original file is served). Every response includesAccept-Ranges: bytesto advertise support.
§Performance (fixed)
- The server root is canonicalized once at startup; per-request resolution takes the already-canonical root as a documented precondition instead of re-canonicalizing (two syscalls plus an allocation) on every single request.
- File responses stream to the client one chunk at a time via a
Bodyimpl backed by a reusedBytesMut; each chunk is handed off viasplit_to(n).freeze()— no per-chunk zero-fill, no second copy of every byte read, and memory use stays bounded to one chunk per in-flight response regardless of file size. - Path resolution’s blocking
canonicalize()syscalls run on Tokio’s blocking thread pool viaspawn_blocking, not directly on the async worker thread handling the request — a slow filesystem lookup for one request no longer stalls every other task scheduled on that same worker thread. - Conditional-request support: the
If-None-Matchheader is honored against the response’s ETag, returning304 Not Modifiedwhen the file hasn’t changed — clients that revalidate get a fast, bodyless response instead of re-downloading the same content.If-Modified-Sinceis deliberately not implemented: an ETag distinguishes representations that a whole-second mtime cannot (two writes inside the same second), so it is the validator to serve. Through 0.24.1 that reasoning did not survive contact with the implementation — the ETag was"<size>-<mtime_secs>", itself whole-second, so rewriting a file within a second of its last write without changing its length reproduced the previous ETag and every revalidating client was told304 Not Modifiedwhile holding stale bytes. 0.25.0 includes sub-second precision ("<size>-<secs>.<nanos>"), making the claim true rather than aspirational. On a filesystem with only second-granular timestamps the nanos component is0and the behavior is what it was — no worse, and no confidence beyond what the filesystem gives.
§Filename decoding
Request paths are percent-decoded and interpreted as UTF-8, so non-ASCII filenames
(é.png, requested as /%C3%A9.png) are servable. If the decoded bytes are not valid
UTF-8, the original still-encoded string is used instead — it will not match a file, so
the request 404s. A filename that is not valid UTF-8 is therefore not servable; macOS
filesystems require UTF-8 names anyway, so this is reachable only on Linux and only for
deliberately-created names.
The segment check and the canonicalize() guard remain the authoritative boundary
regardless of how the name was decoded, and the hidden-segment check above runs on the
decoded path so percent-encoding cannot smuggle a dot past it.
Through 0.28.0 this section claimed decoding produced raw bytes reassembled via
OsStr::from_byteson Unix. It never did —decode_request_pathhas always gone throughdecode_utf8with a fallback to the raw string. Implementing byte-level decoding is a small, self-contained change if a consumer ever needs to serve non-UTF-8 names; documenting what the code does came first.
§Cache control and precompression
- Cache-control default. Every 200/304 file response carries
Cache-Control: no-cache— clients always revalidate against the ETag rather than caching blindly or getting no guidance at all. - Immutable assets.
Server::with_immutable_assets(predicate)takes aFn(&Path) -> bool; paths the predicate matches getCache-Control: public, max-age=31536000, immutableinstead of the default. Correct only for fingerprinted filenames (main.a1b2c3.js) where a content change always produces a new name — caching a mutable filename indefinitely would serve stale content to every client that already has it cached. - Precompressed sidecars. If a client’s
Accept-Encodingnamesbrorgzip(brpreferred when both are accepted at equal weight and both sidecars exist; a higherqwins over that default, sobr;q=0.5, gzipserves gzip) and a sibling<path>.br/<path>.gzexists next to the resolved file, its bytes are served instead with a matchingContent-Encoding. Every file response carriesVary: Accept-Encodingso intermediate caches never serve the wrong variant to a differently-capable client, and the ETag reflects whichever variant was actually served — no compression dependency, a real bandwidth win for static sites that ship prebuilt.gz/.brfiles. The sidecar path is derived by appending an extension to the already-resolved, canonicalized path — never by re-resolving a modified request path — so it can’t become a second traversal surface. Negotiation parsesAccept-Encodingas whole tokens withq-weights (RFC 9110):gzip;q=0refuses gzip rather than selecting it, andbrotlidoes not matchbr— both were live bugs in the substring match used through 0.25.0. The*wildcard deliberately selects nothing: missing a compression opportunity costs bandwidth, while guessing at a wildcard risks sending an encoding the client never asked for.
§CSS/JS bundling and minification (external tools)
mini-static does not bundle or minify CSS/JS itself — it delegates to an external CLI tool you configure, and does not install or manage that tool. This keeps the core crate free of any particular bundler’s dependency weight and release cadence, at the cost of an extra install step for the embedder.
- Opt-in, per language, via a named preset.
Server::with_css_tool(CssTool::LightningCss, options)andServer::with_js_tool(JsTool::Esbuild, options)—#[non_exhaustive]enums so more presets can be added later without breaking existingmatches. Neither is configured by default. bundle/minifyare independentCssOptions/JsOptionstoggles. All four combinations are valid per language: passthrough copy, per-file minify (mirrored 1:1 into the output dir, no@import/module resolution), bundle-only (unminified, concatenated/entry-resolved), or bundle + minify.- CSS bundling discovers every
.cssunder the source folders and concatenates each file’s (optionally@import-resolved, optionally minified) output in sorted path order — the same shape as before, just delegated per-file to the external tool. - JS bundling requires an explicit entry point (
JsOptions::bundle_entry(path, output_name), validated to lie under a registered source folder at configuration time) — unlike CSS, a JS module graph has no well-defined “concatenate everything” meaning. Without a bundle entry, JS runs in per-file mode. - Installation is the embedder’s responsibility.
lightningcss(npm packagelightningcss-cli) and/oresbuild(npm packageesbuild) must be onPATH.Server::run_onchecks this at startup — before the listener binds — and returnsErr(StaticError::PipelineSetup)with an install hint if a configured tool’s binary is missing, rather than silently serving unprocessed files. - Bounded and fails loudly. Every tool invocation has a 30-second timeout; a
timeout, non-zero exit, or missing-binary error is reported with the tool’s own
stderr where available (see
ToolError). - Per-file mode degrades to a raw copy on tool failure (a malformed source file, or the tool crashing on it) — logged loudly, not silently, so the server keeps serving and the browser stays in sync rather than 404ing.
- A failed bundle rebuild leaves the previous good bundle in place — never a partial or corrupt file — and is logged; the next successful rebuild replaces it.
@import/module resolution is delegated entirely to the external tool — mini-static no longer enforces an import-root boundary or depth/file-count ceiling itself (the 30-second timeout is the bound in its place). This is an accepted trade-off: CSS/JS source folders are developer-authored build inputs, not request-time attacker input, unlike the HTTP path resolver above (which remains fully guarded).with_bundle_rootstill exists, but now purely as an extra watch target for triggering CSS rebuilds — not an@importtraversal boundary.*.min.css/*.min.jsbypass per-file mode’s minify step — already-minified files are mirrored as-is.- No request-time transformation. All processing is build-time only, via the source pipeline above — a subprocess call has unbounded latency and doesn’t belong inside HTTP request handling. An embedder that wants CSS/JS served transformed must run the server with the appropriate source folder(s) and tool(s) configured.
§SPA-mode navigation
Opt-in client-side navigation for multi-page sites, unlike live-reload meant to be
usable in production, not just local development: Server::with_spa_mode() (swap
target document.body) or Server::with_spa_root(selector) (swap target the element
matched by CSS selector, for sites with persistent chrome — nav, header, footer —
outside the part that changes per page). Both are off by default; enabling either
injects one small <script> into every served text/html response, the same
splice-before-</body> mechanism with_live_reload() already uses (the two compose —
enabling both injects both scripts).
-
Same-origin link clicks are intercepted and turned into a fetch + DOM swap instead of a full navigation: the target URL is fetched, and — only on a successful
text/htmlresponse — the configured root’sinnerHTMLis replaced with the fetched document’s corresponding content,document.titleis updated, and the new URL is pushed viahistory.pushState. A non-OK response, a non-text/htmlresponse, or a fetch error all fall back to a reallocation.hrefnavigation — spa-mode degrades to normal navigation, it never renders a broken page. -
Excluded from interception: links with a
targetother than empty/_self, adownloadattribute,rel="external", adata-no-spaattribute, or a same-page hash-only href — those always get a normal navigation. Adddata-no-spato any link you want to opt out explicitly, e.g. a link to a large non-HTML file: spa-mode fetches the target once via JS before a content-type mismatch falls back to a real navigation, which then re-requests it — worth avoiding for anything large. -
Animated via the View Transitions API (
document.startViewTransition()) when the browser supports it, and a plain synchronous swap otherwise, so nothing breaks on a browser without support.Server::with_spa_transition(SpaTransition)picks how:SpaTransition::Fade(default) — the browser’s built-in cross-fade, no CSS injected. Customize it yourself via::view-transition-old(root)/::view-transition-new(root).SpaTransition::Slide(SlideOptions)— the outgoing page slides out one side while the incoming page slides in from the other, the same direction for every navigation including the browser Back button. mini-static injects the<style>tag this needs (keyframes, plus amix-blend-mode: normaloverride — without it, the browser’s default cross-fade blend mode washes the two pages into each other where they overlap mid-slide, instead of a clean push) — no site CSS required.SlideOptions(builder-style, defaults reproduce the original slide) configures:duration_ms(u32)— the animation’sanimation-duration, default300.direction(SlideDirection)—Forward(default; exits left, enters from the right) orReverse(exits right, enters from the left).easing(impl Into<String>)— the animation’sanimation-timing-function, default"ease".
Back/forward and forward-click navigations are not distinguished — both animate the same way. A per-direction (Back slides the opposite way from Forward) transition was tried and dropped: it needed a
history.stateposition counter to tell the two browser buttons apart (both fire the samepopstateevent) and proved fiddly and unreliable in practice for the payoff.SpaTransition::Slide’sdirectionis a fixed choice, not one that alternates by navigation direction. -
window.dispatchEvent(new CustomEvent("mini-static:navigate", {detail:{url}}))fires after every client-side navigation (not the initial page load). Listen for it to re-run any per-page initialization:window.addEventListener("mini-static:navigate", (e) => { // e.detail.url is the new page's URL; re-init widgets scoped to the swapped root. });This exists because content swapped in via
innerHTMLnever executes<script>tags it contains — a page whose behavior depends on an inline<script>running on every visit needs that logic wired to this event (in addition to normalDOMContentLoaded/inline execution on the first load), not just the inline tag. -
Back/forward (
popstate) is handled by re-fetching and swapping to the newlocation.href, without pushing a new history entry. -
No JS test harness for this crate covers the actual click/transition/popstate behavior — it’s verified manually via
cargo run --example mini-static. Everything else (script injection, escaping, builder wiring, composition with live-reload) is covered bycargo test.
§Configurable response headers
Server::with_response_header(name, value) sends a fixed header on every response —
200, 304, 404, 405, 301, and the error paths alike. A policy header present on some
statuses and missing from others is worse than none, since the error paths are the ones
an attacker is probing:
use mini_static::Server;
use std::path::Path;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let _server = Server::new(Path::new("./public"))?
.with_response_header("Strict-Transport-Security", "max-age=63072000")?
.with_response_header("Content-Security-Policy", "default-src 'self'")?;
Ok(())
}Name and value are validated when the server is built, not on the first request. Headers
the server computes per response — Content-Length, Content-Type, Content-Encoding,
Content-Range, ETag, Cache-Control, Vary, Accept-Ranges, Allow, Location,
Connection, Transfer-Encoding, X-Content-Type-Options — are refused with
StaticError::Config rather than accepted and silently overridden: a fixed
Content-Length or ETag is a correctness bug, not a policy choice. Use
with_immutable_assets for cache policy.
§HTML injection size cap
Live-reload and spa-mode both splice a <script> into served HTML, which is the one
code path that reads a whole file into memory instead of streaming it in bounded chunks
— and it does so per request, so a large HTML page turns every concurrent request for
it into another full copy in memory. Since spa-mode is a production feature, 0.26.1 caps
injection at 8 MiB: a page over that is streamed unmodified, and the skip is logged
(html injection skipped for /path: … exceeds …) so a silently un-enhanced page is
diagnosable rather than mysterious. The cap is far above any hand-written page or
generator output, so it should never fire on content this feature was built for.
§Logging
Opt-in, off by default. Server::with_request_logging() writes one line per request
to stderr; with_request_logging_to(writer) sends them anywhere else:
GET /index.html 200 512 0.421ms— method, path exactly as received, status, response body bytes (- when the length
isn’t known, as on a live-reload SSE stream), and handling time. Connection-level
failures log connection error: … and accept failures log accept error: …; retrying in …; both were discarded entirely before 0.26.0, so a server refusing every request
looked exactly like one nobody was talking to.
The path is logged undecoded, on purpose: it is attacker-controlled input, and a traversal attempt is the line an operator most needs to see verbatim rather than normalized. Writes are serialized so concurrent responses can’t interleave mid-line, and a failing log sink is ignored rather than allowed to fail a request that was served correctly.
Cargo.toml declares no cargo features at all — default = []. Earlier revisions of
this README advertised err and log features, which never existed.
§Non-goals
-
No directory listing UI — an index page is either present as a real file or the request 404s.
-
No on-the-fly transcoding or image resizing.
-
No built-in compression of arbitrary responses — see precompressed sidecar support above as the intended growth path instead.
-
No TLS. This is an embeddable crate, not a standalone server. Certificate loading, renewal, ALPN, and cipher policy are a large surface with their own release cadence, and adding rustls would several-fold the dependency tree of a crate that currently has seven dependencies. It is also why HTTP/2 and HTTP/3 are absent — browsers reach both over TLS only.
Terminate TLS upstream (reverse proxy, ingress,
stunnel). Note the honest limitation if you cannot:run/run_on/run_ephemeralbind their ownTcpListenerinternally, so there is no listener to wrap. Serving TLS today means drivingServer::handle_requestfrom your own accept loop — which works, but gives up everything the built-in loop provides: the connection-count ceiling, accept-error backoff, per-request header timeout and 64 KiB header cap, request logging, andServerHandle’s graceful drain. That is a worse-secured path than the default one, which is the wrong shape for this crate; letting a caller supply accepted streams while keeping all of the above is planned (PLAN-acceptor.md) and does not require TLS to enter this crate. -
No per-IP rate limiting. The connection ceiling (
Server::with_max_connections, default 1024) plus the per-request header timeout and 64 KiB header cap are the whole DoS posture; anything finer-grained belongs upstream.
Structs§
- Broadcaster
- Broadcasts file change events to multiple subscribers.
- Change
Event - A change event broadcast when a watched file is added, modified, or removed.
- CssOptions
- Configuration for
crate::Server::with_css_tool: independentbundle/minifytoggles, all four combinations valid. - JsOptions
- Configuration for
crate::Server::with_js_tool: independentbundle/minifytoggles, all four combinations valid. - Server
- Server
Handle - A handle to a server started by one of the
Server::run*methods. - Slide
Options - Tunable parameters for
SpaTransition::Slide: how long the animation runs, which way it slides, and its CSS easing curve.
Enums§
- Change
Type - The type of change detected in a watched file.
- CssTool
- External tools mini-static knows how to invoke for CSS bundling/minification.
- Hidden
Files - Whether dot-prefixed request-path segments may be served.
- JsTool
- External tools mini-static knows how to invoke for JS bundling/minification.
- Response
Body - The response body type used by mini-static.
- Slide
Direction - Which side the outgoing page exits toward, and the incoming page enters
from, for
SpaTransition::Slide. The two are always opposite — there is no independent control over the incoming side. - SpaTransition
- How spa-mode animates the swap between pages, set via
crate::Server::with_spa_transition. Both variants use the View Transitions API when the browser supports it, and are a plain synchronous swap (no animation) otherwise. - Static
Error - Errors that can occur during static file serving.
Constants§
- LIVE_
RELOAD_ PATH - The request path
Serverserves the live-reload SSE stream on whencrate::Server::with_live_reloadis enabled.
Functions§
- reload_
event_ frame - Encode a reload event as an SSE (Server-Sent Events) frame.
- resolve
- Resolve a request path under a root directory, canonicalizing the root first.
- resolve_
with_ canonical_ root - Resolve a request path under a pre-canonicalized root.
- start_
watching - Start watching a directory for file changes.