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_minify(self) -> Self

Enable in-memory CSS/JS minification for this server (disabled by default).

A .css/.js/.mjs response is minified at most once per source mtime: a hit serves cached bytes, a miss reads and minifies the file and caches the result (see crate::minify). Files matching *.min.css/*.min.js are served as-is — minifying already-minified input is wasted work at best and a correctness risk at worst. If a precompressed sidecar (see Server::run_on’s docs) matches the request, its bytes are served directly and minification is skipped, since a sidecar already represents whatever a build step decided the final bytes should be. A file that fails to minify (rare malformed CSS/JS) is served unminified rather than failing the request.

When with_live_reload() is also enabled, the cache drops an entry as soon as the same file-change event that drives live-reload arrives, instead of only noticing the change reactively on that file’s next request.

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

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

pub fn with_bundle_root(self, path: &Path) -> Result<Self, StaticError>

Allow CSS @import resolution to reach files outside the served root.

When CSS bundling is enabled, @import statements may resolve to files under path, in addition to files under the served root. This is useful for build pipelines where source partials live in a separate directory tree from the final served output.

Files under path are never directly HTTP-servable: Server::resolve and the request-handling path never consult bundle roots, only the bundler’s @import resolution does. This is purely an @import resolution boundary, not a second served root.

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_css_bundle( self, src_dir: &Path, output_path: &Path, ) -> Result<Self, StaticError>

Enable CSS bundling from a source directory to an output file.

When configured, the server will bundle all CSS files from src_dir into a single output file at output_path (following @import statements within src_dir). If with_live_reload() is also enabled, bundling is re-triggered whenever any CSS file in src_dir changes.

Both paths are canonicalized at configuration time. The source directory must exist; parent directories of the output file are created automatically.

§Errors

Returns Err(StaticError::Io) if either path cannot be canonicalized.

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.
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> ArchivePointee for T

Source§

type ArchivedMetadata = ()

The archived version of the pointer metadata for this type.
Source§

fn pointer_metadata( _: &<T as ArchivePointee>::ArchivedMetadata, ) -> <T as Pointee>::Metadata

Converts some archived metadata to the pointer metadata for itself.
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<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

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<F, W, T, D> Deserialize<With<T, W>, D> for F
where W: DeserializeWith<F, T, D>, D: Fallible + ?Sized, F: ?Sized,

Source§

fn deserialize( &self, deserializer: &mut D, ) -> Result<With<T, W>, <D as Fallible>::Error>

Deserializes using the given deserializer
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> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Pointee for T

Source§

type Metadata = ()

The type for metadata in pointers and references to Self.
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

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