Skip to main content

http_handle/
async_server.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (c) 2026 Sebastien Rousseau
3
4//! Async Tokio server entrypoints.
5//!
6//! This module provides the async accept loop that bridges into the existing request
7//! handling stack.
8
9#[cfg(feature = "async")]
10#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
11use crate::async_runtime::run_blocking;
12#[cfg(feature = "async")]
13#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
14use crate::error::ServerError;
15#[cfg(feature = "async")]
16#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
17use crate::server::Server;
18
19/// Starts an async accept loop backed by Tokio.
20///
21/// Each accepted connection is converted to a standard stream and served via the
22/// existing synchronous connection handler on Tokio's blocking pool.
23#[cfg(feature = "async")]
24#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
25///
26/// # Examples
27///
28/// ```rust,no_run
29/// use http_handle::async_server::start_async;
30/// use http_handle::Server;
31/// # #[tokio::main(flavor = "current_thread")]
32/// # async fn main() {
33/// let server = Server::new("127.0.0.1:8080", ".");
34/// let _ = start_async(server).await;
35/// # }
36/// ```
37///
38/// # Errors
39///
40/// Returns an error when binding or accept fails.
41///
42/// # Panics
43///
44/// This function does not panic.
45pub async fn start_async(server: Server) -> Result<(), ServerError> {
46    let listener = tokio::net::TcpListener::bind(server.address())
47        .await
48        .map_err(ServerError::from)?;
49    loop {
50        let (stream, _) =
51            listener.accept().await.map_err(ServerError::from)?;
52        let server_clone = server.clone();
53        let std_stream =
54            stream.into_std().map_err(ServerError::from)?;
55        drop(tokio::spawn(async move {
56            let _ = run_blocking(move || {
57                crate::server::handle_connection(
58                    std_stream,
59                    &server_clone,
60                )
61            })
62            .await;
63        }));
64    }
65}
66
67#[cfg(all(test, feature = "async"))]
68mod tests {
69    use super::*;
70    use std::io::Write;
71    use std::net::{TcpListener, TcpStream};
72    use tempfile::TempDir;
73    use tokio::time::{Duration, sleep};
74
75    fn free_addr() -> String {
76        let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
77        let addr = listener.local_addr().expect("addr");
78        drop(listener);
79        addr.to_string()
80    }
81
82    #[tokio::test]
83    async fn async_server_accepts_connections() {
84        let root = TempDir::new().expect("tmp");
85        std::fs::write(root.path().join("index.html"), b"hello")
86            .expect("write");
87        std::fs::create_dir(root.path().join("404")).expect("404 dir");
88        std::fs::write(root.path().join("404/index.html"), b"404")
89            .expect("404 page");
90
91        let addr = free_addr();
92        let server = Server::builder()
93            .address(&addr)
94            .document_root(root.path().to_str().expect("path"))
95            .build()
96            .expect("server");
97
98        let task = tokio::spawn(start_async(server));
99        sleep(Duration::from_millis(40)).await;
100
101        let mut stream = TcpStream::connect(&addr).expect("connect");
102        stream
103            .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n")
104            .expect("write");
105        sleep(Duration::from_millis(80)).await;
106
107        task.abort();
108    }
109}