Skip to main content

nidus_http/
server.rs

1//! Application server helpers built on Axum and Tokio.
2
3use std::{future::Future, io, net::SocketAddr, net::ToSocketAddrs};
4
5use axum::Router;
6use nidus_core::Application;
7use tokio::net::TcpListener;
8
9/// Extension methods for attaching HTTP routing to a bootstrapped application.
10pub trait ApplicationHttpExt: Sized {
11    /// Attaches an Axum router to the application.
12    fn with_router(self, router: Router) -> HttpApplication;
13}
14
15impl ApplicationHttpExt for Application {
16    fn with_router(self, router: Router) -> HttpApplication {
17        HttpApplication {
18            application: self,
19            router,
20        }
21    }
22}
23
24/// A bootstrapped Nidus application with an Axum router ready to serve.
25///
26/// All serving methods ([`Self::listen`], [`Self::serve`], and their
27/// `*_with_graceful_shutdown` variants) wrap the router with Axum's
28/// `into_make_service_with_connect_info::<SocketAddr>()`. This populates the
29/// [`axum::extract::ConnectInfo<SocketAddr>`] extension for every connection, so
30/// handlers and identity extractors such as
31/// [`crate::context::client_ip_identity`] classify clients by their real peer
32/// address instead of falling through to the spoofable `X-Forwarded-For` header
33/// or a shared `"anonymous"` bucket.
34pub struct HttpApplication {
35    application: Application,
36    router: Router,
37}
38
39impl HttpApplication {
40    /// Returns the underlying bootstrapped application.
41    pub const fn application(&self) -> &Application {
42        &self.application
43    }
44
45    /// Returns the composed Axum router.
46    pub const fn router(&self) -> &Router {
47        &self.router
48    }
49
50    /// Transforms the composed Axum router while preserving the bootstrapped application.
51    pub fn map_router(self, map: impl FnOnce(Router) -> Router) -> Self {
52        Self {
53            application: self.application,
54            router: map(self.router),
55        }
56    }
57
58    /// Consumes this HTTP application and returns its composed router.
59    pub fn into_router(self) -> Router {
60        self.router
61    }
62
63    /// Binds a TCP listener for this application without starting the server.
64    pub async fn bind<A>(&self, address: A) -> io::Result<TcpListener>
65    where
66        A: ToSocketAddrs,
67    {
68        let address = address
69            .to_socket_addrs()?
70            .next()
71            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "address resolved empty"))?;
72        TcpListener::bind(address).await
73    }
74
75    /// Binds `address` and serves until the server exits.
76    ///
77    /// The router is served with [`axum::extract::ConnectInfo<SocketAddr>`]
78    /// populated for every connection. This method does **not** install a
79    /// graceful-shutdown signal: the server runs until the process is killed.
80    /// For in-flight request draining on a shutdown signal, use
81    /// [`Self::listen_with_graceful_shutdown`] (or [`Self::serve`] /
82    /// [`Self::serve_with_graceful_shutdown`] with a pre-bound listener).
83    pub async fn listen<A>(self, address: A) -> io::Result<()>
84    where
85        A: ToSocketAddrs,
86    {
87        let listener = self.bind(address).await?;
88        axum::serve(
89            listener,
90            self.router
91                .into_make_service_with_connect_info::<SocketAddr>(),
92        )
93        .await
94    }
95
96    /// Binds `address` and serves until `shutdown` completes, draining in-flight
97    /// requests before returning.
98    ///
99    /// `shutdown` is any future that signals termination (for example a
100    /// `SIGTERM`/`Ctrl+C` handler in production). While it is pending the server
101    /// keeps accepting requests; once it resolves Axum stops accepting and waits
102    /// for active connections to finish. [`axum::extract::ConnectInfo`] is
103    /// populated for every connection.
104    pub async fn listen_with_graceful_shutdown<A, F>(
105        self,
106        address: A,
107        shutdown: F,
108    ) -> io::Result<()>
109    where
110        A: ToSocketAddrs,
111        F: Future<Output = ()> + Send + 'static,
112    {
113        let listener = self.bind(address).await?;
114        axum::serve(
115            listener,
116            self.router
117                .into_make_service_with_connect_info::<SocketAddr>(),
118        )
119        .with_graceful_shutdown(shutdown)
120        .await
121    }
122
123    /// Serves the application on a previously bound listener.
124    ///
125    /// Prefer this over [`Self::listen`] when you need to control the bind
126    /// yourself (for example to read the assigned port, set `SO_REUSEPORT`, or
127    /// share a listener). [`axum::extract::ConnectInfo<SocketAddr>`] is
128    /// populated for every connection. Like [`Self::listen`], no shutdown signal
129    /// is installed.
130    pub async fn serve(self, listener: TcpListener) -> io::Result<()> {
131        axum::serve(
132            listener,
133            self.router
134                .into_make_service_with_connect_info::<SocketAddr>(),
135        )
136        .await
137    }
138
139    /// Serves on a previously bound listener until `shutdown` completes, draining
140    /// in-flight requests before returning.
141    ///
142    /// See [`Self::listen_with_graceful_shutdown`] for the shutdown semantics;
143    /// see [`Self::serve`] for why a pre-bound listener is useful.
144    /// [`axum::extract::ConnectInfo<SocketAddr>`] is populated for every
145    /// connection.
146    pub async fn serve_with_graceful_shutdown<F>(
147        self,
148        listener: TcpListener,
149        shutdown: F,
150    ) -> io::Result<()>
151    where
152        F: Future<Output = ()> + Send + 'static,
153    {
154        axum::serve(
155            listener,
156            self.router
157                .into_make_service_with_connect_info::<SocketAddr>(),
158        )
159        .with_graceful_shutdown(shutdown)
160        .await
161    }
162}