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_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