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_minify(self) -> Self
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();Sourcepub fn with_bundle_root(self, path: &Path) -> Result<Self, StaticError>
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.
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_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_bundle(_name) so the bundle path reflects the override.
§Errors
Returns Err(StaticError::Io) if dir cannot be canonicalized, or
Err(StaticError::Traversal) if it overlaps a registered source folder.
Sourcepub fn with_css_bundle(self) -> Self
pub fn with_css_bundle(self) -> Self
Enable the single-file CSS bundle: every .css under the source folders is bundled
(with @import resolution within the source folders and any with_bundle_root dirs)
and minified into <output>/styles.css.
Sourcepub fn with_css_bundle_name(self, name: &str) -> Self
pub fn with_css_bundle_name(self, name: &str) -> Self
Enable the single-file CSS bundle with a custom output file name under the output
dir (default is styles.css, see Server::with_css_bundle).
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 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.
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.
Trait Implementations§
Auto Trait Implementations§
impl !RefUnwindSafe for Server
impl !UnwindSafe for Server
impl Freeze for Server
impl Send for Server
impl Sync for Server
impl Unpin for Server
impl UnsafeUnpin for Server
Blanket Implementations§
Source§impl<T> ArchivePointee for T
impl<T> ArchivePointee for T
Source§type ArchivedMetadata = ()
type ArchivedMetadata = ()
Source§fn pointer_metadata(
_: &<T as ArchivePointee>::ArchivedMetadata,
) -> <T as Pointee>::Metadata
fn pointer_metadata( _: &<T as ArchivePointee>::ArchivedMetadata, ) -> <T as Pointee>::Metadata
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<F, W, T, D> Deserialize<With<T, W>, D> for F
impl<F, W, T, D> Deserialize<With<T, W>, D> for F
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
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