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}