modo/server/http.rs
1use std::time::Duration;
2
3use super::Config;
4use crate::error::Result;
5use crate::runtime::Task;
6
7/// An opaque handle to the running HTTP server.
8///
9/// Implements [`Task`] so it can be passed to the [`crate::run!`] macro for
10/// coordinated graceful shutdown alongside other services.
11///
12/// Obtain a handle by calling [`http()`].
13pub struct HttpServer {
14 shutdown_tx: tokio::sync::oneshot::Sender<()>,
15 handle: tokio::task::JoinHandle<()>,
16 shutdown_timeout: Duration,
17}
18
19impl Task for HttpServer {
20 /// Signal the server to stop accepting new connections and wait for
21 /// in-flight requests to drain.
22 ///
23 /// If the drain does not complete within `shutdown_timeout_secs` (from
24 /// [`Config`]), a warning is logged and the function returns `Ok(())`.
25 async fn shutdown(self) -> Result<()> {
26 let _ = self.shutdown_tx.send(());
27 if tokio::time::timeout(self.shutdown_timeout, self.handle)
28 .await
29 .is_err()
30 {
31 tracing::warn!("server shutdown timed out, forcing exit");
32 }
33 Ok(())
34 }
35}
36
37/// Bind a TCP listener and start serving `router`.
38///
39/// Returns an [`HttpServer`] handle immediately; the server runs on a
40/// background Tokio task. Pass the handle to [`crate::run!`] so it is
41/// shut down gracefully when a signal arrives.
42///
43/// # Errors
44///
45/// Returns [`crate::Error`] if the TCP port cannot be bound.
46///
47/// # Example
48///
49/// ```no_run
50/// use modo::server::{Config, http};
51///
52/// #[tokio::main]
53/// async fn main() -> modo::Result<()> {
54/// let config = Config::default();
55/// let router = modo::axum::Router::new();
56/// let server = http(router, &config).await?;
57/// modo::run!(server).await
58/// }
59/// ```
60///
61/// With a [`HostRouter`](super::HostRouter):
62///
63/// ```no_run
64/// use modo::server::{self, Config, HostRouter};
65///
66/// #[tokio::main]
67/// async fn main() -> modo::Result<()> {
68/// let config = Config::default();
69/// let app = HostRouter::new()
70/// .host("acme.com", modo::axum::Router::new())
71/// .host("*.acme.com", modo::axum::Router::new());
72/// let server = server::http(app, &config).await?;
73/// modo::run!(server).await
74/// }
75/// ```
76pub async fn http(router: impl Into<axum::Router>, config: &Config) -> Result<HttpServer> {
77 let router = router.into();
78 let addr = format!("{}:{}", config.host, config.port);
79 let listener = tokio::net::TcpListener::bind(&addr)
80 .await
81 .map_err(|e| crate::error::Error::internal(format!("failed to bind to {addr}: {e}")))?;
82
83 let local_addr = listener
84 .local_addr()
85 .map_err(|e| crate::error::Error::internal(format!("failed to get local address: {e}")))?;
86
87 tracing::info!("server listening on {local_addr}");
88
89 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
90
91 let handle = tokio::spawn(async move {
92 axum::serve(listener, router.into_make_service())
93 .with_graceful_shutdown(async {
94 let _ = shutdown_rx.await;
95 })
96 .await
97 .ok();
98 });
99
100 Ok(HttpServer {
101 shutdown_tx,
102 handle,
103 shutdown_timeout: Duration::from_secs(config.shutdown_timeout_secs),
104 })
105}