Skip to main content

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}