Skip to main content

soma_http_server/
server.rs

1//! Listener binding and the Axum server run loop.
2//!
3//! Product binaries decide the bind address, build their own composed
4//! `Router`, and choose a shutdown signal (typically
5//! [`crate::shutdown_signal`]). This module owns the mechanical part every
6//! Axum HTTP surface repeats: bind a [`TcpListener`], hand it and the router
7//! to `axum::serve`, and optionally wire graceful shutdown.
8
9use std::fmt;
10use std::future::Future;
11use std::net::SocketAddr;
12
13use axum::Router;
14use tokio::net::{TcpListener, ToSocketAddrs};
15
16/// Error binding a listener or running the server loop.
17///
18/// Non-exhaustive: this crate is shared plumbing consumed by multiple
19/// product surfaces, so new failure variants may be added without that
20/// being a breaking change for callers that only use `?`/`Display`/
21/// `Error::source` rather than exhaustively matching.
22#[derive(Debug)]
23#[non_exhaustive]
24pub enum ServerError {
25    /// Binding `addr` failed (e.g. already in use, permission denied).
26    Bind {
27        addr: String,
28        source: std::io::Error,
29    },
30    /// The `axum::serve` run loop returned an error.
31    Serve(std::io::Error),
32}
33
34impl fmt::Display for ServerError {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        match self {
37            ServerError::Bind { addr, source } => write!(f, "failed to bind {addr}: {source}"),
38            ServerError::Serve(source) => write!(f, "server loop failed: {source}"),
39        }
40    }
41}
42
43impl std::error::Error for ServerError {
44    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
45        match self {
46            ServerError::Bind { source, .. } | ServerError::Serve(source) => Some(source),
47        }
48    }
49}
50
51/// Bind a TCP listener at `addr`.
52///
53/// Accepts anything Tokio can resolve to socket addresses — a `SocketAddr`,
54/// a `"host:port"` string (including a hostname, resolved via DNS), or any
55/// other [`ToSocketAddrs`] implementor — matching `TcpListener::bind`'s own
56/// flexibility so callers don't have to pre-parse a config-supplied address
57/// string.
58///
59/// Pass a literal address with port `0` to let the OS assign an ephemeral
60/// port (useful in tests); read it back with `listener.local_addr()`.
61pub async fn bind<A>(addr: A) -> Result<TcpListener, ServerError>
62where
63    A: ToSocketAddrs + fmt::Display,
64{
65    let description = addr.to_string();
66    TcpListener::bind(addr)
67        .await
68        .map_err(|source| ServerError::Bind {
69            addr: description,
70            source,
71        })
72}
73
74/// Serve `router` on `listener` until the process is killed.
75///
76/// No graceful shutdown — prefer [`serve_with_shutdown`] for anything that
77/// should drain in-flight requests before exiting.
78pub async fn serve(listener: TcpListener, router: Router) -> Result<(), ServerError> {
79    axum::serve(
80        listener,
81        router.into_make_service_with_connect_info::<SocketAddr>(),
82    )
83    .await
84    .map_err(ServerError::Serve)
85}
86
87/// Serve `router` on `listener`, draining in-flight requests once `shutdown`
88/// resolves.
89///
90/// `shutdown` is any future — [`crate::shutdown_signal`] is a ready-made one
91/// that resolves on `Ctrl+C` or `SIGTERM`.
92pub async fn serve_with_shutdown<F>(
93    listener: TcpListener,
94    router: Router,
95    shutdown: F,
96) -> Result<(), ServerError>
97where
98    F: Future<Output = ()> + Send + 'static,
99{
100    axum::serve(
101        listener,
102        router.into_make_service_with_connect_info::<SocketAddr>(),
103    )
104    .with_graceful_shutdown(shutdown)
105    .await
106    .map_err(ServerError::Serve)
107}
108
109#[cfg(test)]
110#[path = "server_tests.rs"]
111mod tests;