diff --git a/src/serve/mod.rs b/src/serve/mod.rs index f1958ef7..fd896e79 100644 --- a/src/serve/mod.rs +++ b/src/serve/mod.rs @@ -7,7 +7,7 @@ use crate::{ watch::WatchSystem, ws, }; -use anyhow::{Context, Result}; +use anyhow::{Context, Result, bail}; use axum::{ body::{Body, Bytes}, extract::{self, ws::WebSocketUpgrade}, @@ -159,16 +159,38 @@ impl ServeSystem { .map(|alias| format!("{alias}:{}", cfg.port)) .collect::>(); + // bind eagerly so bind failures surface here, not once the spawned task is polled + let mut listeners = Vec::with_capacity(addr.len()); + let mut bound = Vec::with_capacity(addr.len()); + for addr in &addr { + let listener = std::net::TcpListener::bind(addr) + // required by axum-server's `from_tcp*` constructors + .and_then(|listener| listener.set_nonblocking(true).map(|()| listener)); + match listener { + Ok(listener) => { + // report the actual address, which may differ (e.g. `--port 0`) + bound.push(listener.local_addr().unwrap_or(*addr)); + listeners.push(listener); + } + // a single unavailable address must not take down the other ones + Err(err) => tracing::warn!("failed to bind to {addr}: {err}"), + } + } + + if listeners.is_empty() { + bail!("failed to bind to any of the requested addresses: {addr:?}"); + } + show_listening( &cfg, - &addr, + &bound, &aliases, &serve_base_url, !cfg.disable_address_lookup, ) .await; - let server = run_server(addr, cfg.tls.clone(), router, shutdown_rx); + let server = run_server(listeners, cfg.tls.clone(), router, shutdown_rx); Ok(tokio::spawn(async move { match server.await { @@ -277,7 +299,7 @@ fn show_address(cache: &mut HashSet, local: bool, address: impl Into, + listeners: Vec, tls: Option, router: Router, mut shutdown_rx: broadcast::Receiver<()>, @@ -296,19 +318,18 @@ async fn run_server( let mut tasks = vec![]; - for addr in addr { + for listener in listeners { let router = router.clone(); let shutdown_handle = shutdown_handle.clone(); match &tls { - Some(tls) => - { + Some(tls) => { #[allow(unreachable_code)] match tls.clone() { #[cfg(feature = "rustls")] TlsConfig::Rustls { config } => { tasks.push( async move { - axum_server::bind_rustls(addr, config) + axum_server::from_tcp_rustls(listener, config)? .handle(shutdown_handle) .serve(router.into_make_service()) .await @@ -320,7 +341,11 @@ async fn run_server( TlsConfig::Native { config } => { tasks.push( async move { - axum_server::bind_openssl(addr, config) + // axum-server 0.8 has no `from_tcp_openssl`; build it from `from_tcp` + the OpenSSL acceptor + axum_server::from_tcp(listener)? + .acceptor(axum_server::tls_openssl::OpenSSLAcceptor::new( + config, + )) .handle(shutdown_handle) .serve(router.into_make_service()) .await @@ -333,7 +358,7 @@ async fn run_server( None => tasks.push( async move { - axum_server::bind(addr) + axum_server::from_tcp(listener)? .handle(shutdown_handle) .serve(router.into_make_service()) .await