diff --git a/src/lib.rs b/src/lib.rs index a070d89..73b3f06 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,7 @@ pub mod startup; /// Logging and tracing setup pub mod telemetry; -type MaybeListener = Option>; +type MaybeListener = Option; fn prepare(listener: MaybeListener) -> startup::Application { dotenvy::dotenv().ok(); @@ -70,11 +70,8 @@ pub async fn run(listener: MaybeListener) -> Result<(), std::io::Error> { } #[cfg(test)] -fn make_random_tcp_listener() -> poem::listener::TcpListener { - let tcp_listener = - std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind a random TCP listener"); - let port = tcp_listener.local_addr().unwrap().port(); - poem::listener::TcpListener::bind(format!("127.0.0.1:{port}")) +fn make_random_tcp_listener() -> std::net::TcpListener { + std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind a random TCP listener") } #[cfg(test)] diff --git a/src/startup.rs b/src/startup.rs index ba6ca06..418f37a 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -6,6 +6,7 @@ //! - Configuring CORS //! - Starting the HTTP server +use poem::listener::{Listener, TcpAcceptor}; use poem::middleware::{AddDataEndpoint, Cors, CorsEndpoint}; use poem::{EndpointExt, Route}; use poem_openapi::OpenApiService; @@ -19,10 +20,21 @@ use crate::{ use crate::middleware::rate_limit::RateLimitEndpoint; -type Server = poem::Server, std::convert::Infallible>; +type Server = poem::Server; + /// The configured application with rate limiting, CORS, and settings data. pub type App = AddDataEndpoint>, Settings>; +struct PreBoundListener(std::net::TcpListener); + +impl Listener for PreBoundListener { + type Acceptor = TcpAcceptor; + + async fn into_acceptor(self) -> std::io::Result { + TcpAcceptor::from_std(self.0) + } +} + /// Application builder that holds the server configuration before running. pub struct Application { server: Server, @@ -135,30 +147,33 @@ impl Application { fn setup_server( settings: &Settings, - tcp_listener: Option>, - ) -> Server { - let tcp_listener = tcp_listener.unwrap_or_else(|| { - let address = format!( - "{}:{}", - settings.application.host, settings.application.port - ); - poem::listener::TcpListener::bind(address) - }); - poem::Server::new(tcp_listener) + tcp_listener: Option, + ) -> (Server, u16, String) { + tcp_listener.map_or_else( + || { + let port = settings.application.port; + let host = settings.application.host.clone(); + let address = format!("{host}:{port}"); + let server = poem::Server::new(poem::listener::TcpListener::bind(address).boxed()); + (server, port, host) + }, + |listener| { + let addr = listener.local_addr().expect("Failed to get bound address"); + let port = addr.port(); + let host = addr.ip().to_string(); + let server = poem::Server::new(PreBoundListener(listener).boxed()); + (server, port, host) + }, + ) } /// Builds a new application with the given settings and optional TCP listener. /// /// If no listener is provided, one will be created based on the settings. #[must_use] - pub fn build( - settings: Settings, - tcp_listener: Option>, - ) -> Self { - let port = settings.application.port; - let host = settings.application.host.clone(); + pub fn build(settings: Settings, tcp_listener: Option) -> Self { + let (server, port, host) = Self::setup_server(&settings, tcp_listener); let app = Self::setup_app(&settings); - let server = Self::setup_server(&settings, tcp_listener); Self { server, app, @@ -243,13 +258,11 @@ mod tests { #[test] fn application_with_custom_listener() { let settings = create_test_settings(); - let tcp_listener = + let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port"); - let port = tcp_listener.local_addr().unwrap().port(); - let listener = poem::listener::TcpListener::bind(format!("127.0.0.1:{port}")); - + let expected_port = listener.local_addr().unwrap().port(); let app = Application::build(settings, Some(listener)); assert_eq!(app.host(), "127.0.0.1"); - assert_eq!(app.port(), 8080); + assert_eq!(app.port(), expected_port); } }