pub struct Server { /* private fields */ }Expand description
A static file server for serving files securely from a root directory.
Server canonicalizes the root directory once at creation time and uses the
canonical form for all subsequent requests, avoiding repeated filesystem calls.
§Security
The server protects against:
- Path traversal attacks (e.g.,
../../etc/passwd) - Accessing files outside the root via symlinks
- Disclosing filesystem structure (traversal and missing files both return 404)
§Cloning
Server is cheap to clone: a PathBuf, a couple of primitives, and an Arc’d
predicate closure. Multiple clones can be used concurrently in async tasks without
synchronization overhead.
§Example
use mini_static::Server;
use std::path::Path;
use std::time::Duration;
let server = Server::new(Path::new("./public"))?;
let (port, _handle) = server.run(Duration::from_secs(30)).await?;
println!("Server running on port {}", port);Implementations§
Source§impl Server
impl Server
Sourcepub fn new(root: &Path) -> Result<Self, StaticError>
pub fn new(root: &Path) -> Result<Self, StaticError>
Create a new server with the given root directory.
Canonicalizes the root once at startup. All subsequent requests use the canonical root without re-canonicalizing it, making this suitable for long-lived servers.
§Errors
Returns Err(StaticError::Io) if the root cannot be canonicalized (e.g., doesn’t exist,
no read permissions).
Sourcepub fn with_max_connections(self, max: usize) -> Self
pub fn with_max_connections(self, max: usize) -> Self
Set the maximum number of connections served concurrently (default 1024).
Once this many connections are in flight, run()’s accept loop stops accepting
new ones — without pausing the accept loop, a client that opens a connection and
sends nothing (see the header-read timeout docs on Server::run_on) could
otherwise be used, in enough parallel copies, to exhaust the process’s file
descriptors or memory with no bound at all.
Sourcepub fn with_live_reload(self) -> Self
pub fn with_live_reload(self) -> Self
Enable live-reload for this server (disabled by default).
Once enabled, the run* methods start a background watcher (mtime polling,
bounded 500ms interval — see crate::start_watching) over the server’s root
the first time the server actually starts accepting connections, and:
- serve a live-reload SSE stream at
crate::LIVE_RELOAD_PATH, broadcasting a change event (withcrate::ChangeType) whenever a served file is added, modified, or removed; - inject a small
<script>into every servedtext/htmlresponse that connects to that stream and reloads the page (or hot-swaps stylesheet<link>s, for CSS changes) — no manual client wiring required.
This is meant for local development, not production: leave it disabled (the
default) for any server serving real traffic. A typical call site gates it behind
#[cfg(debug_assertions)] so a release build never pays for the watcher or the
injected script.
§Example
use mini_static::Server;
use std::path::Path;
let server = Server::new(Path::new("./public"))?;
#[cfg(debug_assertions)]
let server = server.with_live_reload();Sourcepub fn with_immutable_assets<F>(self, predicate: F) -> Self
pub fn with_immutable_assets<F>(self, predicate: F) -> Self
Serve files matching predicate with a long-lived, immutable cache policy
instead of the default Cache-Control: no-cache.
predicate is evaluated against each resolved file’s path; a match sends
Cache-Control: public, max-age=31536000, immutable on that file’s 200 and 304
responses. This is correct only for fingerprinted assets (e.g.
main.a1b2c3.js) where a content change always produces a new filename —
caching a mutable filename indefinitely would serve stale content to every
client that already has it cached.
§Example
use mini_static::Server;
use std::path::Path;
let server = Server::new(Path::new("./public"))?
.with_immutable_assets(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.contains(".fingerprint."))
});Sourcepub fn with_bundle_root(self, path: &Path) -> Result<Self, StaticError>
pub fn with_bundle_root(self, path: &Path) -> Result<Self, StaticError>
Register path as an additional directory whose changes should trigger a CSS
bundle rebuild, alongside the registered source folders.
Useful for build pipelines where CSS partials referenced via @import live in a
separate directory tree from the source folders proper: without registering that
tree here, editing a partial wouldn’t be noticed by the watcher and the bundle
would go stale until something else touched it.
Files under path are never directly HTTP-servable: Server::resolve and the
request-handling path never consult bundle roots. This is purely a watch target,
not a second served root, and — since @import resolution is delegated entirely
to the configured CssTool (see Server::with_css_tool) — not an @import
traversal boundary either; the external tool resolves its own imports with no
root mini-static can enforce.
This method is fallible and canonicalizes the path once at call time, matching
Server::new’s canonicalize-once policy. Call it multiple times to register
more than one external source tree.
§Errors
Returns Err(StaticError::Io) if the path cannot be canonicalized.
Sourcepub fn with_source_folder(self, dir: &Path) -> Result<Self, StaticError>
pub fn with_source_folder(self, dir: &Path) -> Result<Self, StaticError>
Designate dir as a source folder whose changes drive the build pipelines.
Watched when with_live_reload() is enabled; .css files under it feed the single
CSS bundle, .js/.mjs files are minified per-file into the output dir.
Rejected if dir overlaps the output dir or an already-registered source folder: a
source folder that is also the output would feed every pipeline its own output — the
feedback loop this layering exists to prevent.
§Errors
Returns Err(StaticError::Io) if dir cannot be canonicalized, or
Err(StaticError::Traversal) if it overlaps the output dir or another source folder.
Sourcepub fn with_asset_folder(self, dir: &Path) -> Result<Self, StaticError>
pub fn with_asset_folder(self, dir: &Path) -> Result<Self, StaticError>
Designate dir as an asset source folder: every file under it (any extension)
is mirrored byte-identical into the output dir at server startup and on every
live-reload change — no CSS/JS transformation, just a flat copy preserving each
file’s path relative to dir. Use this for hand-authored static files
(index.html, images) that should live outside the served/output dir as
source, the same source/output separation with_source_folder’s CSS/JS
pipelines already have.
Rejected if dir overlaps the output dir or an already-registered
source/asset folder, for the same reason with_source_folder rejects it: a
folder that is also the output would feed the pipeline its own output.
§Errors
Returns Err(StaticError::Io) if dir cannot be canonicalized, or
Err(StaticError::Traversal) if it overlaps the output dir or another
registered source/asset folder.
Sourcepub fn with_output_dir(self, dir: &Path) -> Result<Self, StaticError>
pub fn with_output_dir(self, dir: &Path) -> Result<Self, StaticError>
Designate dir as the output directory processed outputs are written to.
Defaults to the served root. The output dir is never a watcher trigger: pipelines
react to source folders only, so a pipeline’s own output can never re-trigger it.
Call this before with_css_tool so a bundle output path reflects the override.
§Errors
Returns Err(StaticError::Io) if dir cannot be canonicalized, or
Err(StaticError::Traversal) if it overlaps a registered source or asset folder.
Sourcepub fn with_css_tool(self, tool: CssTool, options: CssOptions) -> Self
pub fn with_css_tool(self, tool: CssTool, options: CssOptions) -> Self
Configure CSS bundling/minification via an external tool (disabled by default).
tool is a preset naming the CLI mini-static invokes (see CssTool) —
mini-static does not install or manage the binary, only looks it up on PATH;
Server::run_on fails fast at startup if it’s missing. options selects
bundle/minify independently (see CssOptions):
- Neither: every
.cssunder the source folders is copied through unchanged, mirrored into the output dir. minifyonly: each file is minified independently and mirrored (no@importfollowing).bundleonly: every.cssunder the source folders is discovered,@import-resolved, and concatenated into one output file, unminified.- Both: the bundle above, minified.
§Example
use mini_static::{CssOptions, CssTool, Server};
use std::path::Path;
let server = Server::new(Path::new("./public"))?
.with_css_tool(CssTool::LightningCss, CssOptions::new().bundle(true).minify(true));Sourcepub fn with_js_tool(
self,
tool: JsTool,
options: JsOptions,
) -> Result<Self, StaticError>
pub fn with_js_tool( self, tool: JsTool, options: JsOptions, ) -> Result<Self, StaticError>
Configure JS bundling/minification via an external tool (disabled by default).
tool is a preset naming the CLI mini-static invokes (see JsTool) —
mini-static does not install or manage the binary, only looks it up on PATH;
Server::run_on fails fast at startup if it’s missing. Unlike CSS, JS bundling
requires an explicit entry point (JsOptions::bundle_entry) since a JS module
graph has no well-defined “concatenate everything” meaning; without it, options
runs in per-file mode (every .js/.mjs under the source folders processed and
mirrored independently).
§Errors
Returns Err(StaticError::Io) if options specifies a bundle entry that cannot
be canonicalized, or Err(StaticError::Traversal) if it doesn’t lie under a
registered source folder — checked eagerly here so a bad entry path fails at
configuration time, not on the first rebuild.
§Example
use mini_static::{JsOptions, JsTool, Server};
use std::path::Path;
let server = Server::new(Path::new("./public"))?
.with_source_folder(Path::new("./js-src"))?
.with_js_tool(
JsTool::Esbuild,
JsOptions::new()
.bundle_entry(Path::new("./js-src/main.js"), "bundle.js")
.minify(true),
)?;Sourcepub fn with_prune_output(self) -> Self
pub fn with_prune_output(self) -> Self
Remove stale CSS bundle output at build time — specifically, delete the bundle file when no CSS sources remain, rather than serving an orphan. Applies only to the one-shot startup build, never during live-reload.
Sourcepub async fn build(&self) -> Result<(), StaticError>
pub async fn build(&self) -> Result<(), StaticError>
Run every configured build pipeline (CSS/JS tools, asset folders) once and
return, without starting the HTTP server. A one-shot equivalent of the
startup build run* does automatically — for deploy tooling that wants to
populate the output dir ahead of time (e.g. a cargo run --bin build_static
step before baking a Docker image), mirroring a one-shot content
builder’s build() (e.g. mini_docs::Builder::build()).
§Errors
Err(StaticError::PipelineSetup)if a configured tool’s binary that’s actually needed (bundle or minify enabled) is missing fromPATH— checked before anything runs, same asServer::run_on.Err(StaticError::Build)if a configured pipeline step fails (a tool invocation error, a filesystem error writing output, etc.).
§Example
use mini_static::Server;
use std::path::Path;
let server = Server::new(Path::new("./public"))?;
server.build().await?;Sourcepub fn resolve(&self, request_path: &str) -> Result<PathBuf, StaticError>
pub fn resolve(&self, request_path: &str) -> Result<PathBuf, StaticError>
Resolve a request path under the server’s root.
This is a lower-level API for resolving paths without generating HTTP responses.
For most use cases, prefer Server::handle_request or the run* methods.
§Returns
Ok(PathBuf)if the path resolves to a file within root.Err(StaticError)if the path is invalid, missing, or attempts traversal.
Sourcepub async fn run_on(
&self,
addr: SocketAddr,
header_timeout: Duration,
) -> Result<(u16, ServerHandle), StaticError>
pub async fn run_on( &self, addr: SocketAddr, header_timeout: Duration, ) -> Result<(u16, ServerHandle), StaticError>
Run the server on a specific address with a configurable header-read timeout.
Spawns the server in a background Tokio task and returns immediately with the
assigned port number and a ServerHandle. Call handle.shutdown().await to
stop accepting new connections and wait for in-flight connections to finish.
Dropping the handle instead leaves the server running for the life of the process.
§Header-Read Timeout
Connections that don’t send complete HTTP headers within header_timeout are closed.
This prevents slowloris attacks and resource exhaustion from incomplete requests. The
timeout applies only to the header-read phase — once a complete header block has been
read, the connection is handed off with no further time bound, so long-lived response
bodies (e.g. the live-reload SSE stream from Server::with_live_reload) are not cut
off mid-stream.
§Precompressed Sidecars
If a request’s Accept-Encoding allows br or gzip (preferring br) and a
sibling <path>.br/<path>.gz exists next to the resolved file, its bytes are
served instead with a matching Content-Encoding. Every file response carries
Vary: Accept-Encoding so intermediate caches don’t serve the wrong variant to a
differently-capable client.
§Arguments
addr- Socket address to bind to (e.g.,127.0.0.1:0for loopback ephemeral, or0.0.0.0:8080to bind all interfaces on a fixed port).header_timeout- Maximum time to wait for complete HTTP headers on each connection.
§Returns
Ok((u16, ServerHandle))with the assigned port number and a handle for graceful shutdown.Err(StaticError::Io)if binding to the socket fails.Err(StaticError::PipelineSetup)if a configuredCssTool/JsTool’s binary is not found onPATH. Checked before the listener binds: a deployment whose configured pipeline can never run should fail visibly at boot, not be discovered later as a missing/stale asset.
Sourcepub async fn run(
&self,
header_timeout: Duration,
) -> Result<(u16, ServerHandle), StaticError>
pub async fn run( &self, header_timeout: Duration, ) -> Result<(u16, ServerHandle), StaticError>
Run the server on loopback (127.0.0.1), binding an ephemeral port.
Thin wrapper around Server::run_on — see it for the header-read timeout and
sidecar semantics, and for what the returned ServerHandle does.
Sourcepub async fn run_all(
&self,
port: u16,
header_timeout: Duration,
) -> Result<(u16, ServerHandle), StaticError>
pub async fn run_all( &self, port: u16, header_timeout: Duration, ) -> Result<(u16, ServerHandle), StaticError>
Run the server on all interfaces (0.0.0.0) at port (0 for an ephemeral port).
Useful for containerized deployments and reverse-proxy setups. Thin wrapper
around Server::run_on — see it for the header-read timeout and sidecar
semantics, and for what the returned ServerHandle does.
Sourcepub async fn run_ephemeral(&self) -> Result<(u16, ServerHandle), StaticError>
pub async fn run_ephemeral(&self) -> Result<(u16, ServerHandle), StaticError>
Run the server on loopback with the default 30-second header-read timeout.
The recommended entry point for tests and lightweight services that don’t need a
custom timeout. Thin wrapper around Server::run.
§Example
use mini_static::Server;
use std::path::Path;
let server = Server::new(Path::new("./public"))?;
let (port, handle) = server.run_ephemeral().await?;
println!("Server ready on http://127.0.0.1:{}", port);
handle.shutdown().await;Sourcepub async fn handle_request(
&self,
method: &Method,
request_path: &str,
headers: &HeaderMap,
) -> Response<ResponseBody>
pub async fn handle_request( &self, method: &Method, request_path: &str, headers: &HeaderMap, ) -> Response<ResponseBody>
Produce the HTTP response for a request, streaming file bodies to the client.
This is the crate’s single request-handling path: the run* accept loop calls it,
and so should any async server embedding mini-static as a fallback route (e.g.
mini-unified). It never blocks the calling task — path resolution runs on Tokio’s
blocking-thread pool via spawn_blocking, and the file is read via async I/O.
File responses are backed by FileBody, which hands hyper one 64 KB chunk at a
time as poll_frame is driven: memory use stays bounded to one chunk per in-flight
response regardless of file size.
headers are the request’s headers; If-None-Match (304 on a matching ETag) and
Accept-Encoding (precompressed sidecar selection, see Server::run_on) are the
ones read today. Only GET and HEAD are allowed; anything else gets a 405 with an
Allow header. Missing files and traversal attempts both get an identical 404, so a
response never discloses whether a path exists outside the root.