Skip to main content

Server

Struct Server 

Source
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

Source

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).

Source

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.

Source

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 (with crate::ChangeType) whenever a served file is added, modified, or removed;
  • inject a small <script> into every served text/html response 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();
Source

pub fn with_spa_mode(self) -> Self

Enable spa-mode navigation for this server, swapping document.body on each navigation (disabled by default).

Once enabled, every served text/html response gets a small <script> injected (see Server::with_spa_root for what it does) that treats document.body as the swap target. Calling this after Server::with_spa_root does not clear a previously configured root selector — the two methods set independent fields, so .with_spa_root(sel).with_spa_mode() and .with_spa_mode().with_spa_root(sel) both end up with spa-mode on and root sel. Use this one alone when there’s no persistent chrome to preserve across navigations.

§Example
use mini_static::Server;
use std::path::Path;

let server = Server::new(Path::new("./public"))?.with_spa_mode();
Source

pub fn with_spa_root(self, selector: &str) -> Self

Enable spa-mode navigation for this server, swapping only the element matched by the CSS selector on each navigation (disabled by default; also enables spa-mode the same as Server::with_spa_mode).

Once enabled, every served text/html response gets a small <script> injected that intercepts left-clicks on same-origin <a href> elements (skipping links with a non-_self target, a download attribute, rel="external", a data-no-spa attribute, or a same-page hash-only href) and, instead of a normal navigation:

  • fetches the target URL;
  • on a non-OK or non-text/html response (or a fetch error), falls back to a real location.href navigation — spa-mode never renders a broken page;
  • otherwise replaces the matched element’s innerHTML with the corresponding content from the fetched document, updates the page title, and pushes the new URL via history.pushState, animating the swap with document.startViewTransition() where supported;
  • dispatches a mini-static:navigate CustomEvent on window after every client-side navigation, so page scripts can re-run any per-page initialization that would otherwise only execute once (content swapped in via innerHTML never executes its own <script> tags);
  • handles browser back/forward by re-fetching and swapping to the new location.href.

selector is matched against both the current page and the fetched page; a link click where the selector matches neither falls back to a real navigation, same as a fetch failure. Choose a selector that wraps only the content that varies between pages, leaving persistent chrome (nav/header/footer) outside it so it survives navigation untouched.

This is meant to be usable in production, not just local development (unlike Server::with_live_reload): a click on a link mini-static doesn’t intercept, or on a browser without JS or View Transitions support, still works as a normal navigation.

§Example
use mini_static::Server;
use std::path::Path;

let server = Server::new(Path::new("./public"))?.with_spa_root("#app");
Source

pub fn with_spa_transition(self, transition: SpaTransition) -> Self

Set how spa-mode animates the swap between pages (also enables spa-mode the same as Server::with_spa_mode; default SpaTransition::Fade when spa-mode is enabled without calling this).

SpaTransition::Slide injects its own <style> tag alongside the spa-mode <script> — no site CSS is required. See SpaTransition for what each variant does.

§Example
use mini_static::{Server, SpaTransition};
use std::path::Path;

let server = Server::new(Path::new("./public"))?
    .with_spa_root("#app")
    .with_spa_transition(SpaTransition::Slide);
Source

pub fn with_immutable_assets<F>(self, predicate: F) -> Self
where F: Fn(&Path) -> bool + Send + Sync + 'static,

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."))
    });
Source

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.

Source

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.

Source

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.

Source

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.

Source

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 .css under the source folders is copied through unchanged, mirrored into the output dir.
  • minify only: each file is minified independently and mirrored (no @import following).
  • bundle only: every .css under 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));
Source

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),
    )?;
Source

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.

Source

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 from PATH — checked before anything runs, same as Server::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?;
Source

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.
Source

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:0 for loopback ephemeral, or 0.0.0.0:8080 to 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 configured CssTool/JsTool’s binary is not found on PATH. 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.
Source

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.

Source

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.

Source

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;
Source

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.

Trait Implementations§

Source§

impl Clone for Server

Source§

fn clone(&self) -> Server

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more