Skip to main content

mini_serve/
app.rs

1use std::future::Future;
2use std::net::SocketAddr;
3use std::sync::Arc;
4use std::time::Duration;
5
6use hyper::body::Bytes;
7use hyper::body::Incoming;
8use hyper::server::conn::http1;
9use hyper::{Method, Request, Response, StatusCode};
10use hyper::service::service_fn;
11use hyper_util::rt::TokioIo;
12use hyper_util::rt::TokioTimer;
13use http_body_util::combinators::BoxBody;
14use http_body_util::{BodyExt, Empty, Full};
15use tokio::net::{TcpListener, TcpStream};
16#[cfg(feature = "tls")]
17use tokio_rustls::TlsAcceptor;
18
19use crate::body::{MaxBodySize, DEFAULT_MAX_BODY_SIZE};
20use crate::cors::CorsConfig;
21use crate::error::ServeError;
22use crate::handler::{Handler, Middleware, ResponseBody};
23use crate::router::{QueryParams, Router};
24use crate::state::State;
25
26/// The transport-layer peer address a request arrived from. Inserted into
27/// request extensions by [`App::route_with_peer`] — retrieve it with
28/// `req.extensions().get::<PeerAddr>()`.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct PeerAddr(pub SocketAddr);
31
32const MAX_PATH_LEN: usize = 8_192;
33const MAX_QUERY_LEN: usize = 4_096;
34const DEFAULT_HEADER_READ_TIMEOUT: Duration = Duration::from_secs(30);
35const DEFAULT_MAX_CONNECTIONS: usize = 1024;
36#[cfg(feature = "tls")]
37const DEFAULT_TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
38const ACCEPT_BACKOFF_INITIAL: Duration = Duration::from_millis(10);
39const ACCEPT_BACKOFF_MAX: Duration = Duration::from_secs(1);
40
41#[cfg(test)]
42thread_local! {
43	static ERROR_LOG: std::cell::RefCell<Vec<(u16, String)>> = std::cell::RefCell::new(Vec::new());
44}
45
46#[cfg(test)]
47fn capture_error(code: u16, message: String) {
48	ERROR_LOG.with(|log| log.borrow_mut().push((code, message)));
49}
50
51#[cfg(test)]
52fn take_error_log() -> Vec<(u16, String)> {
53	ERROR_LOG.with(|log| log.borrow_mut().drain(..).collect())
54}
55
56/// A source of accepted TCP connections. Abstracted so the accept-error
57/// backoff below can be exercised against a listener that fails on demand,
58/// without needing to provoke real OS-level accept errors (e.g. EMFILE) in
59/// tests.
60trait TcpAccept {
61	async fn accept(&self) -> std::io::Result<(TcpStream, SocketAddr)>;
62}
63
64impl TcpAccept for TcpListener {
65	async fn accept(&self) -> std::io::Result<(TcpStream, SocketAddr)> {
66		TcpListener::accept(self).await
67	}
68}
69
70/// Exponential backoff for retrying `accept()` after an error, so a
71/// sustained failure (e.g. the process is out of file descriptors) degrades
72/// into periodic retries instead of a CPU-bound busy spin. Resets to the
73/// initial delay as soon as an accept succeeds.
74struct Backoff {
75	delay: Duration,
76}
77
78impl Backoff {
79	fn new() -> Self {
80		Backoff { delay: ACCEPT_BACKOFF_INITIAL }
81	}
82
83	fn next_delay(&mut self) -> Duration {
84		let delay = self.delay;
85		self.delay = (self.delay * 2).min(ACCEPT_BACKOFF_MAX);
86		delay
87	}
88
89	fn reset(&mut self) {
90		self.delay = ACCEPT_BACKOFF_INITIAL;
91	}
92}
93
94async fn accept_with_backoff<L: TcpAccept>(
95	listener: &L,
96	backoff: &mut Backoff,
97) -> (TcpStream, SocketAddr) {
98	loop {
99		match listener.accept().await {
100			Ok(conn) => {
101				backoff.reset();
102				return conn;
103			}
104			Err(_) => {
105				tokio::time::sleep(backoff.next_delay()).await;
106			}
107		}
108	}
109}
110
111pub type ErrorHandler =
112	Arc<dyn Fn(StatusCode, &str) -> Response<ResponseBody> + Send + Sync>;
113
114/// An HTTP server application with typed state, routing, and TLS support.
115///
116/// `App<S>` serves requests by routing them to handlers based on method and path.
117/// All handlers share access to a single `S` value (the app state), cloned as an `Arc`
118/// per request for zero-allocation sharing.
119///
120/// # Example
121///
122/// ```ignore
123/// use mini_serve::App;
124///
125/// #[tokio::main]
126/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
127///     let app = App::new(());
128///     app.bind("127.0.0.1:8080".parse()?).await?;
129///     Ok(())
130/// }
131/// ```
132///
133/// # Features
134///
135/// - **Routing**: Register handlers for (Method, Path) pairs with path parameters (`/users/:id`).
136/// - **State sharing**: All handlers receive `Arc<S>` to the app state.
137/// - **Request extraction**: Parse bodies, extract path/query params, and build responses.
138/// - **CORS**: Optional cross-origin request handling with preflight validation.
139/// - **TLS**: Serve over HTTPS when the `tls` feature is enabled.
140/// - **Graceful shutdown**: Drain in-flight requests before exiting.
141pub struct App<S> {
142	state:               Arc<S>,
143	router:              Arc<Router<S>>,
144	max_body_size:       usize,
145	pub(crate) header_read_timeout: Duration,
146	pub(crate) max_connections:     usize,
147	#[cfg(feature = "tls")]
148	pub(crate) tls_handshake_timeout: Duration,
149	error_handler:       ErrorHandler,
150	cors_config:         Option<CorsConfig>,
151}
152
153fn parse_query(query: Option<&str>) -> QueryParams {
154	let mut map = std::collections::HashMap::new();
155	if let Some(query) = query {
156		for pair in query.split('&').filter(|s| !s.is_empty()) {
157			if let Some((key, value)) = pair.split_once('=') {
158				let key = decode_query_component(key);
159				let value = decode_query_component(value);
160				map.insert(key, value);
161			} else {
162				let pair = decode_query_component(pair);
163				map.insert(pair, String::new());
164			}
165		}
166	}
167	QueryParams(map)
168}
169
170fn decode_query_component(s: &str) -> String {
171	let with_spaces = s.replace('+', " ");
172	percent_encoding::percent_decode_str(&with_spaces)
173		.decode_utf8_lossy()
174		.into_owned()
175}
176
177fn error_response(status: StatusCode, message: &str) -> Response<ResponseBody> {
178	#[cfg(test)]
179	capture_error(status.as_u16(), message.to_string());
180
181	let client_message = if status.is_server_error() {
182		"internal server error"
183	} else {
184		message
185	};
186
187	let body = serde_json::json!({ "message": client_message });
188	let json = serde_json::to_string(&body)
189		.unwrap_or_else(|_| r#"{"message":"internal server error"}"#.to_string());
190	Response::builder()
191		.status(status)
192		.header("content-type", "application/json")
193		.body(BoxBody::new(Full::new(Bytes::from(json)).map_err(|never: std::convert::Infallible| match never {})))
194		.expect("status is valid and headers are static ASCII")
195}
196
197fn default_error_handler() -> ErrorHandler {
198	Arc::new(error_response)
199}
200
201/// Address for ephemeral test/dev binds. Deliberately loopback-only —
202/// unlike a production bind, callers never choose this address, so it must
203/// not expose the listener beyond the local machine.
204fn ephemeral_bind_addr() -> SocketAddr {
205	(std::net::Ipv4Addr::LOCALHOST, 0).into()
206}
207
208impl<S: Send + Sync + 'static> App<S> {
209	/// Create a new app with shared state.
210	///
211	/// The state is wrapped in an `Arc` and shared with every request handler
212	/// as `State::from_arc()`. Route registration is done via `RouteBuilder`.
213	pub fn new(state: S) -> Self {
214		App {
215			state:              Arc::new(state),
216			router:             Arc::new(Router::new()),
217			max_body_size:       DEFAULT_MAX_BODY_SIZE,
218			header_read_timeout: DEFAULT_HEADER_READ_TIMEOUT,
219			max_connections:     DEFAULT_MAX_CONNECTIONS,
220			#[cfg(feature = "tls")]
221			tls_handshake_timeout: DEFAULT_TLS_HANDSHAKE_TIMEOUT,
222			error_handler:       default_error_handler(),
223			cors_config:         None,
224		}
225	}
226
227	/// Get an `Arc` to the app state.
228	///
229	/// Useful for spawning background tasks or accessing state outside the
230	/// request-response loop.
231	pub fn state_arc(&self) -> Arc<S> {
232		Arc::clone(&self.state)
233	}
234
235	pub async fn route(&self, req: Request<Incoming>) -> Response<ResponseBody> {
236		self.route_with(req, None).await
237	}
238
239	/// Route a request, additionally exposing the transport-layer peer
240	/// address to handlers and middleware via `req.extensions().get::<PeerAddr>()`.
241	///
242	/// Used internally by the accept loops, which always know the peer
243	/// address; exposed publicly for callers embedding `App` into their own
244	/// connection-handling code with a real peer address available.
245	pub async fn route_with_peer(&self, req: Request<Incoming>, peer: SocketAddr) -> Response<ResponseBody> {
246		self.route_with(req, Some(peer)).await
247	}
248
249	async fn route_with(&self, req: Request<Incoming>, peer: Option<SocketAddr>) -> Response<ResponseBody> {
250		let method = req.method().clone();
251		let resp = self.route_inner(req, peer).await;
252
253		// Strip the body uniformly across every branch above (success,
254		// handler error, 404, 405) rather than only the success path.
255		// hyper's HTTP/1 server already refuses to write a body to the wire
256		// for HEAD regardless of what we return here, so this doesn't change
257		// observable behavior — it just avoids handing hyper a body (e.g. a
258		// freshly-built error JSON payload) that would only be discarded.
259		if method == Method::HEAD {
260			let (parts, _) = resp.into_parts();
261			Response::from_parts(parts, BoxBody::new(Empty::new().map_err(|never: std::convert::Infallible| match never {})))
262		} else {
263			resp
264		}
265	}
266
267	async fn route_inner(&self, req: Request<Incoming>, peer: Option<SocketAddr>) -> Response<ResponseBody> {
268		if req.uri().path().len() > MAX_PATH_LEN {
269			return (self.error_handler)(StatusCode::BAD_REQUEST, "path too long");
270		}
271		if req.uri().query().map(|q| q.len()).unwrap_or(0) > MAX_QUERY_LEN {
272			return (self.error_handler)(StatusCode::BAD_REQUEST, "query string too long");
273		}
274
275		let method = req.method().clone();
276		let path = req.uri().path().to_string();
277		let state = State::from_arc(Arc::clone(&self.state));
278		let query_params = parse_query(req.uri().query());
279
280		// Extract Origin header for CORS before consuming request
281		let req_origin = req
282			.headers()
283			.get("origin")
284			.and_then(|v| v.to_str().ok())
285			.map(|s| s.to_string());
286
287		// Handle CORS preflight only for existing routes
288		if method == Method::OPTIONS && req_origin.is_some() {
289			if let Some(cfg) = &self.cors_config {
290				if self.router.path_exists(&path) {
291					let requested_headers = req
292						.headers()
293						.get("access-control-request-headers")
294						.and_then(|v| v.to_str().ok());
295					let allowed = self.allowed_methods_with_head(&path);
296					return cfg.preflight_response(req_origin.as_deref(), requested_headers, &allowed);
297				}
298			}
299		}
300
301		let method_to_match = if method == Method::HEAD {
302			Method::GET
303		} else {
304			method.clone()
305		};
306
307		match self.router.match_route(&method_to_match, &path) {
308			Some((handler, params)) => {
309				let mut req = req;
310				req.extensions_mut().insert(query_params);
311				req.extensions_mut().insert(params);
312				req.extensions_mut().insert(MaxBodySize(self.max_body_size));
313				if let Some(peer) = peer {
314					req.extensions_mut().insert(PeerAddr(peer));
315				}
316				match handler(req, state).await {
317					Ok(mut resp) => {
318						if let Some(cfg) = &self.cors_config {
319							cfg.apply_to_response(&mut resp, req_origin.as_deref());
320						}
321						resp
322					}
323					Err(e) => (self.error_handler)(
324						StatusCode::from_u16(e.code)
325							.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
326						&e.message,
327					),
328				}
329			}
330			None => {
331				let allowed = self.allowed_methods_with_head(&path);
332				if !allowed.is_empty() {
333					let mut method_strs: Vec<&str> = allowed.iter().map(|m| m.as_str()).collect();
334					method_strs.sort();
335					method_strs.dedup();
336					let allow_header = method_strs.join(", ");
337					let mut resp = (self.error_handler)(StatusCode::METHOD_NOT_ALLOWED, "method not allowed");
338					if let Ok(val) = allow_header.parse() {
339						resp.headers_mut().insert("allow", val);
340					}
341					resp
342				} else {
343					(self.error_handler)(StatusCode::NOT_FOUND, "not found")
344				}
345			}
346		}
347	}
348
349	/// Every method the router accepts for `path`, plus `HEAD` whenever
350	/// `GET` is one of them (hyper's HTTP/1 server answers `HEAD` by running
351	/// the `GET` handler and discarding the body — see `route()` above —
352	/// so `HEAD` is always implicitly valid alongside `GET`). Shared by the
353	/// CORS preflight branch and the plain 405 branch so the two can never
354	/// disagree about what a path actually accepts.
355	fn allowed_methods_with_head(&self, path: &str) -> Vec<Method> {
356		let mut allowed = self.router.allowed_methods(path);
357		if allowed.contains(&Method::GET) {
358			allowed.push(Method::HEAD);
359		}
360		allowed
361	}
362
363	/// Bind to an ephemeral port and serve in the background.
364	///
365	/// Returns the assigned port number. The server runs in a spawned task
366	/// and serves until the process exits. For graceful shutdown, use `run()`.
367	/// Binds to 127.0.0.1 only—safe for development and testing.
368	pub async fn bind_ephemeral(self) -> Result<u16, ServeError> {
369		let listener = TcpListener::bind(ephemeral_bind_addr())
370			.await
371			.map_err(|e| ServeError::new(500, format!("failed to bind to ephemeral port: {e}")))?;
372		let port = listener
373			.local_addr()
374			.map_err(|e| ServeError::new(500, format!("failed to get assigned port: {e}")))?
375			.port();
376		let app = Arc::new(self);
377		tokio::spawn(async move {
378			serve_inner(listener, app).await;
379		});
380		Ok(port)
381	}
382
383	/// Bind to an ephemeral port with TLS and serve in the background.
384	///
385	/// Requires the `tls` feature. Returns the assigned port number.
386	/// The server runs in a spawned task and enforces TLS handshake timeouts
387	/// to prevent stalled clients from blocking the accept loop.
388	#[cfg(feature = "tls")]
389	pub async fn bind_tls_ephemeral(
390		self,
391		config: Arc<rustls::ServerConfig>,
392	) -> Result<u16, ServeError> {
393		let listener = TcpListener::bind(ephemeral_bind_addr())
394			.await
395			.map_err(|e| ServeError::new(500, format!("failed to bind to ephemeral port: {e}")))?;
396		let port = listener
397			.local_addr()
398			.map_err(|e| ServeError::new(500, format!("failed to get assigned port: {e}")))?
399			.port();
400		let acceptor = TlsAcceptor::from(config);
401		let app = Arc::new(self);
402		tokio::spawn(async move {
403			serve_tls_inner(listener, app, acceptor).await;
404		});
405		Ok(port)
406	}
407
408	/// Serve `listener` until `shutdown` resolves, then drain in-flight
409	/// connections and return. The production entry point for callers that
410	/// want control over the shutdown trigger (tests, custom signals); see
411	/// [`App::bind`] for the OS-signal convenience wrapper.
412	pub async fn run<F>(self, listener: TcpListener, shutdown: F) -> Result<(), ServeError>
413	where
414		F: Future<Output = ()> + Send + 'static,
415	{
416		let app = Arc::new(self);
417		serve_with_shutdown(listener, app, shutdown).await;
418		Ok(())
419	}
420
421	/// Bind `addr` and serve until SIGINT or SIGTERM, then drain in-flight
422	/// connections and return. Unlike [`App::bind_ephemeral`], `addr` is
423	/// caller-chosen — e.g. `0.0.0.0:$PORT` for a platform like fly.io that
424	/// routes external traffic to the process directly.
425	pub async fn bind(self, addr: SocketAddr) -> Result<(), ServeError> {
426		let listener = TcpListener::bind(addr)
427			.await
428			.map_err(|e| ServeError::new(500, format!("failed to bind to {addr}: {e}")))?;
429		self.run(listener, signal_shutdown()).await
430	}
431
432	/// TLS variant of [`App::run`].
433	#[cfg(feature = "tls")]
434	pub async fn run_tls<F>(
435		self,
436		listener: TcpListener,
437		config: Arc<rustls::ServerConfig>,
438		shutdown: F,
439	) -> Result<(), ServeError>
440	where
441		F: Future<Output = ()> + Send + 'static,
442	{
443		let acceptor = TlsAcceptor::from(config);
444		let app = Arc::new(self);
445		serve_tls_with_shutdown(listener, app, acceptor, shutdown).await;
446		Ok(())
447	}
448
449	/// TLS variant of [`App::bind`].
450	#[cfg(feature = "tls")]
451	pub async fn bind_tls(
452		self,
453		addr: SocketAddr,
454		config: Arc<rustls::ServerConfig>,
455	) -> Result<(), ServeError> {
456		let listener = TcpListener::bind(addr)
457			.await
458			.map_err(|e| ServeError::new(500, format!("failed to bind to {addr}: {e}")))?;
459		self.run_tls(listener, config, signal_shutdown()).await
460	}
461}
462
463impl App<()> {
464	pub fn stateless() -> Self {
465		App::new(())
466	}
467}
468
469/// Wires an accepted (and, for TLS, already-handshaken) connection up to the
470/// hyper HTTP/1 service and drives it to completion. Shared by every accept
471/// loop below so the framing/timeout setup is defined exactly once.
472async fn serve_connection<S, IO>(io: IO, app: Arc<App<S>>, header_read_timeout: Duration, peer: SocketAddr)
473where
474	S: Send + Sync + 'static,
475	IO: hyper::rt::Read + hyper::rt::Write + Unpin + 'static,
476{
477	let svc = service_fn(move |req: Request<Incoming>| {
478		let app = app.clone();
479		async move {
480			Ok::<_, hyper::Error>(app.route_with_peer(req, peer).await)
481		}
482	});
483	let mut builder = http1::Builder::new();
484	builder.timer(TokioTimer::new());
485	builder.header_read_timeout(header_read_timeout);
486	let conn = builder.serve_connection(io, svc);
487	let _ = conn.await;
488}
489
490async fn serve_inner<S: Send + Sync + 'static>(
491	listener: TcpListener,
492	app: Arc<App<S>>,
493) {
494	let header_read_timeout = app.header_read_timeout;
495	let semaphore = Arc::new(tokio::sync::Semaphore::new(app.max_connections));
496	let mut backoff = Backoff::new();
497	loop {
498		let (stream, peer) = accept_with_backoff(&listener, &mut backoff).await;
499		let sem = semaphore.clone();
500		let permit = match sem.acquire_owned().await {
501			Ok(p) => p,
502			Err(_) => continue,
503		};
504		let app = app.clone();
505		tokio::spawn(async move {
506			let _permit = permit;
507			serve_connection(TokioIo::new(stream), app, header_read_timeout, peer).await;
508		});
509	}
510}
511
512/// TLS variant of [`serve_inner`]. Each accepted TCP connection must complete
513/// the TLS handshake within `handshake_timeout`; a stalled or malicious
514/// client that never sends a ClientHello is dropped without blocking the
515/// accept loop from serving other connections.
516#[cfg(feature = "tls")]
517async fn serve_tls_inner<S: Send + Sync + 'static>(
518	listener: TcpListener,
519	app: Arc<App<S>>,
520	acceptor: TlsAcceptor,
521) {
522	let header_read_timeout = app.header_read_timeout;
523	let handshake_timeout = app.tls_handshake_timeout;
524	let semaphore = Arc::new(tokio::sync::Semaphore::new(app.max_connections));
525	let mut backoff = Backoff::new();
526	loop {
527		let (stream, peer) = accept_with_backoff(&listener, &mut backoff).await;
528		let sem = semaphore.clone();
529		let permit = match sem.acquire_owned().await {
530			Ok(p) => p,
531			Err(_) => continue,
532		};
533		let app = app.clone();
534		let acceptor = acceptor.clone();
535		tokio::spawn(async move {
536			let _permit = permit;
537			let tls_stream = match tokio::time::timeout(handshake_timeout, acceptor.accept(stream)).await {
538				Ok(Ok(s)) => s,
539				Ok(Err(_)) | Err(_) => return,
540			};
541			serve_connection(TokioIo::new(tls_stream), app, header_read_timeout, peer).await;
542		});
543	}
544}
545
546/// Accept a connection and reserve it a connection-limit permit, retrying
547/// transient `accept()` errors with [`Backoff`]. Returns `None` only if the
548/// semaphore itself has been closed (never happens in normal operation, since
549/// nothing ever calls `close()` on it — handled so a caller can still fail
550/// safely rather than panic).
551///
552/// Deliberately returns one future that covers accept *and* permit
553/// acquisition, so a caller can race the whole thing against a shutdown
554/// signal in a single `select!`. Racing only the accept and leaving permit
555/// acquisition as a bare `.await` afterward was the prior implementation's
556/// bug: once a connection was accepted but was waiting on a saturated
557/// semaphore, that wait was invisible to the `select!` and shutdown could not
558/// preempt it.
559async fn accept_and_permit<L: TcpAccept>(
560	listener: &L,
561	backoff: &mut Backoff,
562	semaphore: &Arc<tokio::sync::Semaphore>,
563) -> Option<(TcpStream, SocketAddr, tokio::sync::OwnedSemaphorePermit)> {
564	loop {
565		let (stream, peer) = match listener.accept().await {
566			Ok(conn) => {
567				backoff.reset();
568				conn
569			}
570			Err(_) => {
571				tokio::time::sleep(backoff.next_delay()).await;
572				continue;
573			}
574		};
575		return match semaphore.clone().acquire_owned().await {
576			Ok(permit) => Some((stream, peer, permit)),
577			Err(_) => None,
578		};
579	}
580}
581
582/// Set up a shutdown future that fires on SIGINT or SIGTERM.
583async fn signal_shutdown() {
584	let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
585		.expect("failed to install SIGINT handler");
586	let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
587		.expect("failed to install SIGTERM handler");
588
589	tokio::select! {
590		_ = sigint.recv() => {}
591		_ = sigterm.recv() => {}
592	}
593}
594
595/// Graceful-shutdown accept loop: accepts and serves connections until
596/// `shutdown` resolves, then stops accepting immediately and waits only for
597/// already-spawned connections to finish before returning.
598///
599/// The accept-and-permit step and the shutdown signal are the two arms of a
600/// single `select!`, so shutdown can win the race — and cancel a pending
601/// accept or a permit wait cleanly — at any point, not just between
602/// iterations.
603async fn serve_with_shutdown<S, F>(listener: TcpListener, app: Arc<App<S>>, shutdown: F)
604where
605	S: Send + Sync + 'static,
606	F: Future<Output = ()> + Send + 'static,
607{
608	let header_read_timeout = app.header_read_timeout;
609	let semaphore = Arc::new(tokio::sync::Semaphore::new(app.max_connections));
610	let mut backoff = Backoff::new();
611	let mut join_set: tokio::task::JoinSet<()> = tokio::task::JoinSet::new();
612	let mut shutdown_pin = std::pin::pin!(shutdown);
613	let mut shutting_down = false;
614
615	loop {
616		if !shutting_down {
617			tokio::select! {
618				accepted = accept_and_permit(&listener, &mut backoff, &semaphore) => {
619					match accepted {
620						Some((stream, peer, permit)) => {
621							let app = app.clone();
622							join_set.spawn(async move {
623								let _permit = permit;
624								serve_connection(TokioIo::new(stream), app, header_read_timeout, peer).await;
625							});
626						}
627						None => shutting_down = true,
628					}
629				}
630				_ = shutdown_pin.as_mut() => {
631					shutting_down = true;
632				}
633			}
634			continue;
635		}
636
637		match join_set.join_next().await {
638			Some(_) => continue,
639			None => break,
640		}
641	}
642}
643
644/// TLS variant of [`serve_with_shutdown`]. The TLS handshake (already bounded
645/// by `handshake_timeout`, see [`serve_tls_inner`]) happens inside the
646/// spawned task, after the permit is held — the accept/permit race against
647/// shutdown is identical to the plain case.
648#[cfg(feature = "tls")]
649async fn serve_tls_with_shutdown<S, F>(
650	listener: TcpListener,
651	app: Arc<App<S>>,
652	acceptor: TlsAcceptor,
653	shutdown: F,
654) where
655	S: Send + Sync + 'static,
656	F: Future<Output = ()> + Send + 'static,
657{
658	let header_read_timeout = app.header_read_timeout;
659	let handshake_timeout = app.tls_handshake_timeout;
660	let semaphore = Arc::new(tokio::sync::Semaphore::new(app.max_connections));
661	let mut backoff = Backoff::new();
662	let mut join_set: tokio::task::JoinSet<()> = tokio::task::JoinSet::new();
663	let mut shutdown_pin = std::pin::pin!(shutdown);
664	let mut shutting_down = false;
665
666	loop {
667		if !shutting_down {
668			tokio::select! {
669				accepted = accept_and_permit(&listener, &mut backoff, &semaphore) => {
670					match accepted {
671						Some((stream, peer, permit)) => {
672							let app = app.clone();
673							let acceptor = acceptor.clone();
674							join_set.spawn(async move {
675								let _permit = permit;
676								let tls_stream = match tokio::time::timeout(handshake_timeout, acceptor.accept(stream)).await {
677									Ok(Ok(s)) => s,
678									Ok(Err(_)) | Err(_) => return,
679								};
680								serve_connection(TokioIo::new(tls_stream), app, header_read_timeout, peer).await;
681							});
682						}
683						None => shutting_down = true,
684					}
685				}
686				_ = shutdown_pin.as_mut() => {
687					shutting_down = true;
688				}
689			}
690			continue;
691		}
692
693		match join_set.join_next().await {
694			Some(_) => continue,
695			None => break,
696		}
697	}
698}
699
700/// Builder for configuring routes and settings before creating an `App`.
701///
702/// `RouteBuilder` uses a fluent API to register routes, configure CORS, and adjust
703/// server settings. Call `.seal()` to produce the final `App<S>`.
704///
705/// # Example
706///
707/// ```ignore
708/// use mini_serve::{RouteBuilder, handler, body};
709/// use hyper::Response;
710/// use hyper::body::Bytes;
711///
712/// #[tokio::main]
713/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
714///     let app = RouteBuilder::stateless()
715///         .get("/health", handler(|_, _| async {
716///             Ok(Response::new(body(Bytes::from("OK"))))
717///         }))
718///         .seal();
719///
720///     app.bind("127.0.0.1:8080".parse()?).await?;
721///     Ok(())
722/// }
723/// ```
724#[must_use = "RouteBuilder does nothing until .seal() is called"]
725pub struct RouteBuilder<S> {
726	state:               Arc<S>,
727	router:              Router<S>,
728	max_body_size:       usize,
729	header_read_timeout: Duration,
730	max_connections:     usize,
731	#[cfg(feature = "tls")]
732	tls_handshake_timeout: Duration,
733	error_handler:       ErrorHandler,
734	cors_config:         Option<CorsConfig>,
735	middlewares:         Vec<Middleware<S>>,
736}
737
738impl<S: Send + Sync + 'static> RouteBuilder<S> {
739	/// Create a new builder with shared state.
740	pub fn new(state: S) -> Self {
741		RouteBuilder {
742			state:               Arc::new(state),
743			router:              Router::new(),
744			max_body_size:       DEFAULT_MAX_BODY_SIZE,
745			header_read_timeout: DEFAULT_HEADER_READ_TIMEOUT,
746			max_connections:     DEFAULT_MAX_CONNECTIONS,
747			#[cfg(feature = "tls")]
748			tls_handshake_timeout: DEFAULT_TLS_HANDSHAKE_TIMEOUT,
749			error_handler:       default_error_handler(),
750			cors_config:         None,
751			middlewares:         Vec::new(),
752		}
753	}
754
755	/// Register a middleware. Applies to every route registered *after* this
756	/// call — middlewares wrap in registration order, so the first `.wrap()`
757	/// call becomes the outermost layer and runs first.
758	pub fn wrap(mut self, middleware: Middleware<S>) -> Self {
759		self.middlewares.push(middleware);
760		self
761	}
762
763	fn apply_middlewares(&self, handler: Handler<S>) -> Handler<S> {
764		self.middlewares.iter().rev().fold(handler, |acc, mw| mw(acc))
765	}
766
767	/// Set the maximum request body size in bytes (default: 2 MiB).
768	pub fn with_max_body_size(mut self, max: usize) -> Self {
769		self.max_body_size = max;
770		self
771	}
772
773	/// Set the header read timeout (default: 30 seconds).
774	pub fn with_header_read_timeout(mut self, d: Duration) -> Self {
775		self.header_read_timeout = d;
776		self
777	}
778
779	/// Set the TLS handshake timeout (default: 10 seconds).
780	/// Requires the `tls` feature.
781	#[cfg(feature = "tls")]
782	pub fn with_tls_handshake_timeout(mut self, d: Duration) -> Self {
783		self.tls_handshake_timeout = d;
784		self
785	}
786
787	/// Set the maximum concurrent connections (default: 1024).
788	pub fn with_max_connections(mut self, max: usize) -> Self {
789		self.max_connections = max;
790		self
791	}
792
793	pub fn with_error_handler(
794		mut self,
795		f: impl Fn(StatusCode, &str) -> Response<ResponseBody> + Send + Sync + 'static,
796	) -> Self {
797		self.error_handler = Arc::new(f);
798		self
799	}
800
801	pub fn with_cors(mut self, config: CorsConfig) -> Self {
802		self.cors_config = Some(config);
803		self
804	}
805
806	pub fn get(mut self, path: &str, handler: Handler<S>) -> Self {
807		let handler = self.apply_middlewares(handler);
808		self.router.insert(Method::GET, path, handler);
809		self
810	}
811
812	pub fn post(mut self, path: &str, handler: Handler<S>) -> Self {
813		let handler = self.apply_middlewares(handler);
814		self.router.insert(Method::POST, path, handler);
815		self
816	}
817
818	pub fn put(mut self, path: &str, handler: Handler<S>) -> Self {
819		let handler = self.apply_middlewares(handler);
820		self.router.insert(Method::PUT, path, handler);
821		self
822	}
823
824	pub fn delete(mut self, path: &str, handler: Handler<S>) -> Self {
825		let handler = self.apply_middlewares(handler);
826		self.router.insert(Method::DELETE, path, handler);
827		self
828	}
829
830	pub fn seal(self) -> App<S> {
831		App {
832			state:              self.state,
833			router:             Arc::new(self.router),
834			max_body_size:       self.max_body_size,
835			header_read_timeout: self.header_read_timeout,
836			max_connections:     self.max_connections,
837			#[cfg(feature = "tls")]
838			tls_handshake_timeout: self.tls_handshake_timeout,
839			error_handler:       self.error_handler,
840			cors_config:         self.cors_config,
841		}
842	}
843}
844
845impl RouteBuilder<()> {
846	pub fn stateless() -> Self {
847		RouteBuilder::new(())
848	}
849}
850
851#[cfg(test)]
852mod tests {
853	use super::*;
854	use std::sync::Mutex;
855	use std::sync::atomic::{AtomicUsize, Ordering};
856	use http_body_util::BodyExt;
857
858	#[test]
859	fn ephemeral_bind_addr_is_loopback_only() {
860		assert_eq!(
861			ephemeral_bind_addr().ip(),
862			std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
863		);
864	}
865
866	#[test]
867	fn backoff_delays_double_up_to_a_cap() {
868		let mut backoff = Backoff::new();
869
870		assert_eq!(backoff.next_delay(), ACCEPT_BACKOFF_INITIAL);
871		assert_eq!(backoff.next_delay(), ACCEPT_BACKOFF_INITIAL * 2);
872		assert_eq!(backoff.next_delay(), ACCEPT_BACKOFF_INITIAL * 4);
873
874		// Keep pulling well past the point it must have saturated.
875		let mut last = Duration::ZERO;
876		for _ in 0..20 {
877			last = backoff.next_delay();
878		}
879		assert_eq!(last, ACCEPT_BACKOFF_MAX);
880	}
881
882	#[test]
883	fn backoff_reset_returns_to_initial_delay() {
884		let mut backoff = Backoff::new();
885		backoff.next_delay();
886		backoff.next_delay();
887		backoff.reset();
888		assert_eq!(backoff.next_delay(), ACCEPT_BACKOFF_INITIAL);
889	}
890
891	/// Fails `accept()` a fixed number of times, recording the (paused,
892	/// virtual) instant of each attempt, before delegating to a real
893	/// listener so the caller can eventually succeed.
894	struct FlakyListener {
895		inner:              TcpListener,
896		remaining_failures: AtomicUsize,
897		attempts:           Mutex<Vec<tokio::time::Instant>>,
898	}
899
900	impl TcpAccept for FlakyListener {
901		async fn accept(&self) -> std::io::Result<(TcpStream, SocketAddr)> {
902			self.attempts.lock().unwrap().push(tokio::time::Instant::now());
903			if self.remaining_failures.fetch_sub(1, Ordering::SeqCst) > 0 {
904				Err(std::io::Error::other("simulated accept error"))
905			} else {
906				TcpAccept::accept(&self.inner).await
907			}
908		}
909	}
910
911	#[tokio::test(start_paused = true)]
912	async fn accept_loop_backs_off_between_repeated_errors_instead_of_busy_spinning() {
913		let inner = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
914		let addr = inner.local_addr().unwrap();
915
916		let flaky = FlakyListener {
917			inner,
918			remaining_failures: AtomicUsize::new(5),
919			attempts: Mutex::new(Vec::new()),
920		};
921
922		tokio::spawn(async move {
923			let _ = TcpStream::connect(addr).await;
924		});
925
926		let mut backoff = Backoff::new();
927		accept_with_backoff(&flaky, &mut backoff).await;
928
929		let recorded = flaky.attempts.lock().unwrap();
930		assert_eq!(recorded.len(), 6, "5 failures then 1 success");
931
932		let expected_gaps = [
933			ACCEPT_BACKOFF_INITIAL,
934			ACCEPT_BACKOFF_INITIAL * 2,
935			ACCEPT_BACKOFF_INITIAL * 4,
936			ACCEPT_BACKOFF_INITIAL * 8,
937			ACCEPT_BACKOFF_INITIAL * 16,
938		];
939		for (i, expected) in expected_gaps.iter().enumerate() {
940			let gap = recorded[i + 1] - recorded[i];
941			assert_eq!(
942				gap, *expected,
943				"gap between attempt {i} and {} should reflect the backoff delay, not a busy spin",
944				i + 1
945			);
946		}
947	}
948
949	#[tokio::test]
950	async fn error_handler_sanitizes_5xx_in_response_body() {
951		take_error_log(); // clear any prior state
952		let resp = error_response(StatusCode::INTERNAL_SERVER_ERROR, "raw db connection string leaked");
953
954		let (parts, body) = resp.into_parts();
955		assert_eq!(parts.status, StatusCode::INTERNAL_SERVER_ERROR);
956
957		let collected = body.collect().await.unwrap();
958		let bytes = collected.to_bytes();
959		let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
960		let msg = json.get("message").and_then(|v| v.as_str()).unwrap();
961		assert_eq!(msg, "internal server error", "5xx message should be sanitized");
962	}
963
964	#[test]
965	fn error_handler_captures_5xx_message_in_log() {
966		take_error_log(); // clear any prior state
967		let sensitive_msg = "raw db connection string leaked";
968		error_response(StatusCode::INTERNAL_SERVER_ERROR, sensitive_msg);
969
970		let log = take_error_log();
971		assert_eq!(log.len(), 1);
972		assert_eq!(log[0].0, 500);
973		assert_eq!(log[0].1, sensitive_msg);
974	}
975
976	#[tokio::test]
977	async fn error_handler_passes_through_4xx_in_response_body() {
978		take_error_log(); // clear any prior state
979		let msg = "bad request";
980		let resp = error_response(StatusCode::BAD_REQUEST, msg);
981
982		let (parts, body) = resp.into_parts();
983		assert_eq!(parts.status, StatusCode::BAD_REQUEST);
984
985		let collected = body.collect().await.unwrap();
986		let bytes = collected.to_bytes();
987		let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
988		let response_msg = json.get("message").and_then(|v| v.as_str()).unwrap();
989		assert_eq!(response_msg, msg, "4xx message should pass through");
990	}
991
992	#[test]
993	fn error_handler_logs_4xx_messages() {
994		take_error_log(); // clear any prior state
995		let msg = "bad request";
996		error_response(StatusCode::BAD_REQUEST, msg);
997
998		let log = take_error_log();
999		assert_eq!(log.len(), 1);
1000		assert_eq!(log[0].0, 400);
1001		assert_eq!(log[0].1, msg);
1002	}
1003}