pub struct Server { /* private fields */ }Expand description
The main Actus server.
Implementations§
Source§impl Server
impl Server
Sourcepub fn new(router: Router) -> Self
pub fn new(router: Router) -> Self
Create a server for router with default settings: no middleware, no
CORS, the default body cap, and no DoS limits. Configure it with the
with_* builder methods, then call run.
Sourcepub fn router(&self) -> Arc<Router> ⓘ
pub fn router(&self) -> Arc<Router> ⓘ
The router this server serves, shared.
Lets application code keep using the route tree after Server::new
has consumed it — most usefully from a Middleware that wants the
framework’s own longest-prefix matcher instead of re-deriving it. A
declaration-keyed gate is the motivating case:
let server = Server::new(router);
let router = server.router(); // Arc<Router>
let server = server.with_middleware(FloorGate { router });
// …in the gate's `before`:
// if let Some(rm) = self.router.match_controller(&req.path_parts) {
// if rm.controller.actus_expects() == Some("credential") && … { … }
// }The clone is an Arc clone; a gate’s extra match_controller call is
one map lookup per path segment on top of the one the server already
does. See the README’s “Route families” section for the full pattern.
Sourcepub fn with_middleware(self, middleware: impl Middleware + 'static) -> Self
pub fn with_middleware(self, middleware: impl Middleware + 'static) -> Self
Adds a middleware to the server’s request processing chain.
Sourcepub fn with_cors(self, cors: CorsLayer) -> Self
pub fn with_cors(self, cors: CorsLayer) -> Self
Enables CORS with the given policy. The server then answers preflight
(OPTIONS) requests itself and adds the Access-Control-* headers to
every cross-origin response (including error responses). See
CorsLayer.
Sourcepub fn with_compression(self, layer: CompressionLayer) -> Self
pub fn with_compression(self, layer: CompressionLayer) -> Self
Enables response compression (gzip / brotli). For each response Actus
picks an encoding from the request’s Accept-Encoding and compresses
buffered, compressible bodies above the layer’s size threshold. See
CompressionLayer. (Requires the compression feature.)
Sourcepub fn with_max_body_bytes(self, max: usize) -> Self
pub fn with_max_body_bytes(self, max: usize) -> Self
Caps the request body Actus will buffer (default
DEFAULT_MAX_BODY_BYTES = 2 MiB). A larger body is rejected with
413 Payload Too Large before it can exhaust memory — the limit
bounds buffered bytes, so it also covers chunked bodies that lie about
(or omit) Content-Length.
0 is accepted and means “reject every non-empty body” — typically
only useful on a strictly-GET surface that should never see a body.
Sourcepub fn with_request_timeout(self, d: Duration) -> Self
pub fn with_request_timeout(self, d: Duration) -> Self
Cap the total time any single request may take — body parse,
middleware before, handler, middleware after, and finalization
combined. An over-budget request is aborted (the handler’s future
is dropped) and the client gets 504 Gateway Timeout. No timeout
is set by default.
Scope. The timer covers the request/response exchange. A
WebSocket upgrade succeeds inside the timer (the 101 is the
response); the post-upgrade conversation runs in its own task and
is not bound by this timeout.
Effect of an over-budget request. When the timer elapses the in-flight future is dropped, which cancels whatever the handler was awaiting (DB query, channel recv, etc.). The 504 reply is one-shot — the after-chain doesn’t run on it (by definition, some component upstream was unresponsive; running more risks hanging again).
Sourcepub fn with_drain_deadline(self, d: Duration) -> Self
pub fn with_drain_deadline(self, d: Duration) -> Self
Override the grace period for in-flight connections after a
shutdown signal (default DEFAULT_DRAIN_DEADLINE = 30 s).
Anything still running at the deadline is hard-aborted via
JoinSet::shutdown. Use a longer value for surfaces that hold
long-lived connections (large file downloads, WebSockets);
a shorter value for fast-iteration dev workflows. Duration::ZERO
aborts every in-flight task immediately.
Sourcepub fn with_max_connections(self, n: usize) -> Self
pub fn with_max_connections(self, n: usize) -> Self
Cap concurrent connection tasks at n. While the cap is held, the
accept loop pauses on permit acquisition; new SYNs queue in the
kernel’s accept backlog and (once that fills, governed by
SOMAXCONN) get dropped at the OS level. No userland reject /
no-503-per-conn cost — the kernel handles the spillover.
Each connection task holds its permit until it ends, including the
post-handshake WebSocket conversation. Size accordingly: a
with_max_connections(N) server can hold N long-lived WebSockets
before it stops accepting new connections of any kind.
Unbounded by default (no semaphore installed).
Sourcepub fn with_max_inflight_body_bytes(self, n: usize) -> Self
pub fn with_max_inflight_body_bytes(self, n: usize) -> Self
Cap the total bytes being buffered across all in-flight body reads
at n. Each request reserves its per-request cap (see
with_max_body_bytes) from this global budget upfront; if the
budget is exhausted, the request is refused with 503 Service Unavailable (via WebError::Busy) and a short Retry-After.
Together with Self::with_max_connections this puts a hard
ceiling on the framework’s memory under adversarial load:
with_max_connections(C) * with_max_body_bytes(B) is the worst
case absent this knob; with it, the ceiling is min(C * B, this value).
Pre-reserving the per-request cap over-counts (a 1 KB request reserves up to its full cap); the alternative — incremental per-chunk byte accounting — is more code for the same effective ceiling, and a request that has already started buffering can’t be sensibly aborted partway through anyway.
n is clamped to u32::MAX internally (Tokio’s Semaphore
permit count uses u32); for practical deployments this is no
limit (4 GiB).
Unbounded by default.
Sourcepub fn with_header_read_timeout(self, d: Duration) -> Self
pub fn with_header_read_timeout(self, d: Duration) -> Self
Bound how long after starting to read request headers we’ll wait
before dropping the connection. Forwards to hyper’s
http1::Builder::header_read_timeout. Catches slowloris (sending
headers one byte at a time) and clients that TCP-connect-and-send-
nothing — the most common file-descriptor-exhaustion attack on a
keep-alive HTTP server.
Note: hyper 1.x doesn’t have a separate “idle between requests” timeout (after a complete request, an idle keep-alive connection stays open until either side closes or the OS-level TCP keep-alive fires). If that matters for your deployment, either disable keep-alive entirely upstream of Actus or rely on the OS knobs.
No timeout by default (hyper’s default).
Sourcepub async fn run(self, port: u16) -> Result<(), ServerError>
pub async fn run(self, port: u16) -> Result<(), ServerError>
Runs the server on 127.0.0.1:port (loopback only). For a different
bind address — e.g. 0.0.0.0:port to accept connections from other
hosts in a container — use Server::run_on.
Listens for SIGTERM/SIGINT (Unix) or Ctrl-C (cross-platform) and shuts down gracefully: stops accepting new connections, signals in-flight connections to finish, and waits up to 30 seconds for them to drain before returning.
Sourcepub async fn run_on(self, addr: SocketAddr) -> Result<(), ServerError>
pub async fn run_on(self, addr: SocketAddr) -> Result<(), ServerError>
Like Server::run but binds an arbitrary address. Pass
0.0.0.0:port (or [::]:port) to accept connections from other hosts.
Sourcepub async fn run_with_shutdown(
self,
port: u16,
shutdown: impl Future<Output = ()> + Send + 'static,
) -> Result<(), ServerError>
pub async fn run_with_shutdown( self, port: u16, shutdown: impl Future<Output = ()> + Send + 'static, ) -> Result<(), ServerError>
Like Server::run but with a custom shutdown trigger (a future that,
when it resolves, starts the graceful drain). Binds 127.0.0.1:port;
see Server::run_with_shutdown_on for a custom bind address. Useful
for tests or for embedding the server in a larger supervision tree.
Sourcepub async fn run_with_shutdown_on(
self,
addr: SocketAddr,
shutdown: impl Future<Output = ()> + Send + 'static,
) -> Result<(), ServerError>
pub async fn run_with_shutdown_on( self, addr: SocketAddr, shutdown: impl Future<Output = ()> + Send + 'static, ) -> Result<(), ServerError>
Bind addr, then serve exactly as
Server::run_with_shutdown_listener — the general form — does.
Server::run, Server::run_on, and Server::run_with_shutdown
are thin wrappers over this; this is a thin bind over the listener
form.
Drain bound. Once shutdown resolves the server stops accepting
and signals every in-flight connection to wind down. The drain
deadline defaults to DEFAULT_DRAIN_DEADLINE (30 s); override
with Server::with_drain_deadline. Anything still running at
the deadline is hard-aborted via JoinSet::shutdown. In particular,
long-lived connections (WebSockets, slow downloads, kept-alive idle
clients) and any connection task that raced the shutdown notification
and missed it both get aborted at the deadline rather than draining
gracefully.
Sourcepub async fn run_listener(
self,
listener: TcpListener,
) -> Result<(), ServerError>
pub async fn run_listener( self, listener: TcpListener, ) -> Result<(), ServerError>
Like Server::run_on but serves on a listener the caller already
bound (or inherited), with the default SIGTERM / SIGINT shutdown
trigger. See Server::run_with_shutdown_listener for what a
pre-bound listener buys and how to adopt an inherited one.
Sourcepub async fn run_with_shutdown_listener(
self,
listener: TcpListener,
shutdown: impl Future<Output = ()> + Send + 'static,
) -> Result<(), ServerError>
pub async fn run_with_shutdown_listener( self, listener: TcpListener, shutdown: impl Future<Output = ()> + Send + 'static, ) -> Result<(), ServerError>
The most general form: serve on a listener the caller already bound
(or inherited) until shutdown resolves, then drain. Every other
run* method is a wrapper over this one.
Why hand the server a listener. Two cases the bind-it-yourself forms can’t express:
- Socket activation — a supervisor (systemd’s
LISTEN_FDSprotocol, launchd, …) owns the listening socket and passes it to the process it spawns. Because the socket outlives the process, connections arriving during a restart queue in the kernel’s accept backlog instead of being refused, and the next process serves them. - Race-free embedding and tests — bind
127.0.0.1:0, keep the listener, and pass it in; no bind-drop-rebind window in which the port can be lost, and requests may connect before the accept loop even starts (the kernel queues them).
An inherited std::net::TcpListener must be set non-blocking before
conversion — tokio requires it, and a supervisor passes the fd
blocking:
inherited.set_nonblocking(true)?;
let listener = tokio::net::TcpListener::from_std(inherited)?;
server.run_with_shutdown_listener(listener, std::future::pending()).awaitDrain bound. As Server::run_with_shutdown_on: once shutdown
resolves the server stops accepting (the listener is dropped — under
socket activation the socket stays open in the supervisor, which is
the point) and in-flight connections get Server::with_drain_deadline
(default DEFAULT_DRAIN_DEADLINE = 30 s) to finish before being
aborted. A streaming response that never completes (SSE) always rides
to the deadline.