Skip to main content

mini_serve/
app.rs

1use std::future::Future;
2use std::net::SocketAddr;
3#[cfg(feature = "tls")]
4use std::pin::Pin;
5use std::io::Write as _;
6use std::sync::{Arc, Mutex};
7use std::time::Duration;
8
9use hyper::header::{HeaderName, HeaderValue};
10use hyper::body::Body as HttpBody;
11use hyper::body::Bytes;
12use hyper::body::Incoming;
13use hyper::server::conn::http1;
14use hyper::{Method, Request, Response, StatusCode};
15use hyper::service::service_fn;
16use hyper_util::rt::TokioIo;
17use hyper_util::rt::TokioTimer;
18use http_body_util::combinators::BoxBody;
19use http_body_util::{BodyExt, Empty, Full};
20use tokio::net::{TcpListener, TcpStream};
21#[cfg(feature = "tls")]
22use tokio_rustls::TlsAcceptor;
23
24use crate::body::{MaxBodySize, DEFAULT_MAX_BODY_SIZE};
25use crate::cors::CorsConfig;
26use crate::error::ServeError;
27use crate::handler::{Handler, Middleware, OnUpgrade, ResponseBody};
28use crate::router::{QueryParams, Router};
29use crate::state::State;
30
31/// The transport-layer peer address a request arrived from. Inserted into
32/// request extensions by [`App::route_with_peer`] — retrieve it with
33/// `req.extensions().get::<PeerAddr>()`.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct PeerAddr(pub SocketAddr);
36
37const MAX_PATH_LEN: usize = 8_192;
38const MAX_QUERY_LEN: usize = 4_096;
39const DEFAULT_HEADER_READ_TIMEOUT: Duration = Duration::from_secs(30);
40const DEFAULT_MAX_CONNECTIONS: usize = 1024;
41
42/// Headers the connection layer owns, refused as fixed values by
43/// [`RouteBuilder::with_response_header`]. A caller-supplied `Content-Length` would
44/// contradict the body hyper is about to write; the other two describe framing this
45/// crate does not choose.
46const CONNECTION_OWNED_HEADERS: [HeaderName; 3] = [
47	hyper::header::CONTENT_LENGTH,
48	hyper::header::CONNECTION,
49	hyper::header::TRANSFER_ENCODING,
50];
51/// Grace period `serve_loop` allows in-flight connections to finish after the shutdown
52/// signal, before whatever is left is aborted.
53///
54/// The drain used to be unbounded — `join_next()` until the set emptied — so a single
55/// wedged handler, or a client holding a long-lived streaming response, kept the process
56/// alive forever and made `bind()` un-returnable. A2 says every wait states its ceiling;
57/// shutdown is no exception. Matches `mini-static`'s constant of the same name and value.
58const DEFAULT_SHUTDOWN_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
59#[cfg(feature = "tls")]
60const DEFAULT_TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
61/// How long the connection task waits for an upgrade to complete after the `101` has been
62/// written, before giving up and releasing the connection's permit.
63///
64/// A2: this wait needs a ceiling like any other. A handler can return `101` to a client
65/// that never actually asked to upgrade, in which case hyper's upgrade future never
66/// resolves — and without a bound the task would hold its permit forever, which is a leak
67/// reachable from the network by the very mechanism meant to prevent one.
68const UPGRADE_HANDOFF_TIMEOUT: Duration = Duration::from_secs(10);
69const ACCEPT_BACKOFF_INITIAL: Duration = Duration::from_millis(10);
70const ACCEPT_BACKOFF_MAX: Duration = Duration::from_secs(1);
71
72#[cfg(test)]
73thread_local! {
74	static ERROR_LOG: std::cell::RefCell<Vec<(u16, String)>> = const { std::cell::RefCell::new(Vec::new()) };
75}
76
77#[cfg(test)]
78fn capture_error(code: u16, message: String) {
79	ERROR_LOG.with(|log| log.borrow_mut().push((code, message)));
80}
81
82#[cfg(test)]
83fn take_error_log() -> Vec<(u16, String)> {
84	ERROR_LOG.with(|log| log.borrow_mut().drain(..).collect())
85}
86
87/// A source of accepted TCP connections. Abstracted so the accept-error
88/// backoff below can be exercised against a listener that fails on demand,
89/// without needing to provoke real OS-level accept errors (e.g. EMFILE) in
90/// tests.
91trait TcpAccept {
92	async fn accept(&self) -> std::io::Result<(TcpStream, SocketAddr)>;
93}
94
95impl TcpAccept for TcpListener {
96	async fn accept(&self) -> std::io::Result<(TcpStream, SocketAddr)> {
97		TcpListener::accept(self).await
98	}
99}
100
101/// Exponential backoff for retrying `accept()` after an error, so a
102/// sustained failure (e.g. the process is out of file descriptors) degrades
103/// into periodic retries instead of a CPU-bound busy spin. Resets to the
104/// initial delay as soon as an accept succeeds.
105struct Backoff {
106	delay: Duration,
107}
108
109impl Backoff {
110	fn new() -> Self {
111		Backoff { delay: ACCEPT_BACKOFF_INITIAL }
112	}
113
114	fn next_delay(&mut self) -> Duration {
115		let delay = self.delay;
116		self.delay = (self.delay * 2).min(ACCEPT_BACKOFF_MAX);
117		delay
118	}
119
120	fn reset(&mut self) {
121		self.delay = ACCEPT_BACKOFF_INITIAL;
122	}
123}
124
125pub type ErrorHandler =
126	Arc<dyn Fn(StatusCode, &str) -> Response<ResponseBody> + Send + Sync>;
127
128/// Where request and failure lines go.
129///
130/// `App` is shared behind an `Arc` and its connections run on independent tasks, so the
131/// sink is shared rather than duplicated; the mutex serializes writes so two tasks
132/// finishing at once cannot interleave mid-line and produce an entry belonging to
133/// neither.
134pub(crate) type LogSink = Arc<Mutex<Box<dyn std::io::Write + Send>>>;
135
136/// An HTTP server application with typed state, routing, and TLS support.
137///
138/// `App<S>` serves requests by routing them to handlers based on method and path.
139/// All handlers share access to a single `S` value (the app state), cloned as an `Arc`
140/// per request for zero-allocation sharing.
141///
142/// # Example
143///
144/// ```ignore
145/// use mini_serve::App;
146///
147/// #[tokio::main]
148/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
149///     let app = App::new(());
150///     app.bind("127.0.0.1:8080".parse()?).await?;
151///     Ok(())
152/// }
153/// ```
154///
155/// # Features
156///
157/// - **Routing**: Register handlers for (Method, Path) pairs with path parameters (`/users/:id`).
158/// - **State sharing**: All handlers receive `Arc<S>` to the app state.
159/// - **Request extraction**: Parse bodies, extract path/query params, and build responses.
160/// - **CORS**: Optional cross-origin request handling with preflight validation.
161/// - **TLS**: Serve over HTTPS when the `tls` feature is enabled.
162/// - **Graceful shutdown**: Drain in-flight requests, bounded by a grace period, before
163///   exiting.
164pub struct App<S> {
165	state:               Arc<S>,
166	log:                 Option<LogSink>,
167	extra_headers:       Arc<Vec<(HeaderName, HeaderValue)>>,
168	router:              Arc<Router<S>>,
169	max_body_size:       usize,
170	pub(crate) upgrades_enabled:    bool,
171	pub(crate) header_read_timeout: Duration,
172	pub(crate) max_connections:     usize,
173	#[cfg(feature = "tls")]
174	pub(crate) tls_handshake_timeout: Duration,
175	error_handler:       ErrorHandler,
176	cors_config:         Option<CorsConfig>,
177}
178
179/// Report a connection task that ended by panicking.
180///
181/// The join result was previously dropped on the floor, so a panicking handler showed up
182/// as a dropped connection with no record anywhere — the client could not tell it from a
183/// network fault and the operator could not tell it happened at all.
184pub(crate) fn report_if_panicked(sink: &Option<LogSink>, joined: Result<(), tokio::task::JoinError>) {
185	let Err(e) = joined else {
186		return;
187	};
188	if e.is_panic() {
189		log_line(sink, format_args!("connection task panicked: {e}"));
190	}
191}
192
193/// Write `line` to `sink`, if there is one.
194///
195/// A poisoned mutex and a failed write are both ignored: neither is a reason to fail a
196/// request that was otherwise served, and a logger that can take down the server is
197/// worse than one that occasionally drops a line.
198pub(crate) fn log_line(sink: &Option<LogSink>, line: std::fmt::Arguments<'_>) {
199	let Some(sink) = sink else {
200		return;
201	};
202	if let Ok(mut out) = sink.lock() {
203		let _ = writeln!(out, "{line}");
204		let _ = out.flush();
205	}
206}
207
208/// Parse a non-empty query string into its decoded key/value pairs.
209///
210/// A repeated key resolves to its last value, which is what `?a=1&a=2` means here.
211/// Callers guard on emptiness — see the call site in `route_inner`.
212fn parse_query(query: &str) -> QueryParams {
213	let mut map = std::collections::HashMap::new();
214	for pair in query.split('&').filter(|s| !s.is_empty()) {
215		if let Some((key, value)) = pair.split_once('=') {
216			let key = decode_query_component(key);
217			let value = decode_query_component(value);
218			map.insert(key, value);
219		} else {
220			let pair = decode_query_component(pair);
221			map.insert(pair, String::new());
222		}
223	}
224	QueryParams(map)
225}
226
227fn decode_query_component(s: &str) -> String {
228	let with_spaces = s.replace('+', " ");
229	percent_encoding::percent_decode_str(&with_spaces)
230		.decode_utf8_lossy()
231		.into_owned()
232}
233
234fn error_response(status: StatusCode, message: &str) -> Response<ResponseBody> {
235	#[cfg(test)]
236	capture_error(status.as_u16(), message.to_string());
237
238	let client_message = if status.is_server_error() {
239		"internal server error"
240	} else {
241		message
242	};
243
244	let body = serde_json::json!({ "message": client_message });
245	let json = serde_json::to_string(&body)
246		.unwrap_or_else(|_| r#"{"message":"internal server error"}"#.to_string());
247	let mut resp = Response::new(BoxBody::new(
248		Full::new(Bytes::from(json)).map_err(|never: std::convert::Infallible| match never {}),
249	));
250	*resp.status_mut() = status;
251	// `insert` rather than the builder's `header()`, which appends and therefore scans
252	// the map first. Same reasoning and the same scope limit as `response::json` — see
253	// the note there before copying this anywhere else.
254	resp.headers_mut().insert(
255		hyper::header::CONTENT_TYPE,
256		HeaderValue::from_static("application/json"),
257	);
258	resp
259}
260
261fn default_error_handler() -> ErrorHandler {
262	Arc::new(error_response)
263}
264
265/// Address for ephemeral test/dev binds. Deliberately loopback-only —
266/// unlike a production bind, callers never choose this address, so it must
267/// not expose the listener beyond the local machine.
268fn ephemeral_bind_addr() -> SocketAddr {
269	(std::net::Ipv4Addr::LOCALHOST, 0).into()
270}
271
272impl<S: Send + Sync + 'static> App<S> {
273	/// Create a new app with shared state.
274	///
275	/// The state is wrapped in an `Arc` and shared with every request handler
276	/// as `State::from_arc()`. Route registration is done via `RouteBuilder`.
277	pub fn new(state: S) -> Self {
278		App {
279			state:              Arc::new(state),
280			router:             Arc::new(Router::new()),
281			max_body_size:       DEFAULT_MAX_BODY_SIZE,
282			header_read_timeout: DEFAULT_HEADER_READ_TIMEOUT,
283			upgrades_enabled:    false,
284			log:                 None,
285			extra_headers:       Arc::new(Vec::new()),
286			max_connections:     DEFAULT_MAX_CONNECTIONS,
287			#[cfg(feature = "tls")]
288			tls_handshake_timeout: DEFAULT_TLS_HANDSHAKE_TIMEOUT,
289			error_handler:       default_error_handler(),
290			cors_config:         None,
291		}
292	}
293
294	/// Get an `Arc` to the app state.
295	///
296	/// Useful for spawning background tasks or accessing state outside the
297	/// request-response loop.
298	pub fn state_arc(&self) -> Arc<S> {
299		Arc::clone(&self.state)
300	}
301
302	pub async fn route(&self, req: Request<Incoming>) -> Response<ResponseBody> {
303		self.route_with(req, None).await
304	}
305
306	/// Route a request, additionally exposing the transport-layer peer
307	/// address to handlers and middleware via `req.extensions().get::<PeerAddr>()`.
308	///
309	/// Used internally by the accept loops, which always know the peer
310	/// address; exposed publicly for callers embedding `App` into their own
311	/// connection-handling code with a real peer address available.
312	pub async fn route_with_peer(&self, req: Request<Incoming>, peer: SocketAddr) -> Response<ResponseBody> {
313		self.route_with(req, Some(peer)).await
314	}
315
316	async fn route_with(&self, req: Request<Incoming>, peer: Option<SocketAddr>) -> Response<ResponseBody> {
317		let method = req.method().clone();
318		// Read before `req` is consumed: every exit below needs it, not just the one
319		// that used to apply CORS. `get` returns the *first* `Origin` when a request
320		// carries several — this is the only read of that header, so the preflight
321		// branch and `finalize` cannot end up reflecting different ones.
322		let req_origin = req
323			.headers()
324			.get(hyper::header::ORIGIN)
325			.and_then(|v| v.to_str().ok())
326			.map(|s| s.to_string());
327
328		let mut resp = self.route_inner(req, peer, req_origin.as_deref()).await;
329		self.finalize(&mut resp, req_origin.as_deref());
330
331		// Strip the body uniformly across every branch above (success,
332		// handler error, 404, 405) rather than only the success path.
333		// hyper's HTTP/1 server already refuses to write a body to the wire
334		// for HEAD regardless of what we return here, so this doesn't change
335		// observable behavior — it just avoids handing hyper a body (e.g. a
336		// freshly-built error JSON payload) that would only be discarded.
337		if method == Method::HEAD {
338			let (mut parts, body) = resp.into_parts();
339			// RFC 9110 §9.3.2: HEAD must report the `Content-Length` its GET would.
340			// Handlers that set the header themselves (`json`, for one) already carry
341			// it in `parts`; handlers that returned a body and let hyper derive the
342			// length lose it here, because the body carrying that length is exactly
343			// what is being dropped. Take the length from the body before discarding
344			// it, so `curl -I` and any CDN sizing a resource get the real answer
345			// instead of nothing.
346			if !parts.headers.contains_key(hyper::header::CONTENT_LENGTH) {
347				if let Some(len) = HttpBody::size_hint(&body).exact() {
348					parts.headers.insert(hyper::header::CONTENT_LENGTH, len.into());
349				}
350			}
351			Response::from_parts(parts, BoxBody::new(Empty::new().map_err(|never: std::convert::Infallible| match never {})))
352		} else {
353			resp
354		}
355	}
356
357	/// The single point every response passes through before reaching hyper.
358	///
359	/// CORS headers used to be applied in exactly one of six exit paths — the branch
360	/// where a handler returned `Ok`. A cross-origin request that 404'd, 405'd, or
361	/// errored therefore came back *without* them, so the browser reported an opaque
362	/// CORS failure and the caller never learned the real status. Applying here fixes
363	/// that by construction: a new exit path cannot forget what it never had to
364	/// remember.
365	///
366	/// `apply_to_response` inserts rather than appends, so a preflight response that
367	/// already carries these headers is unchanged by passing through again.
368	fn finalize(&self, resp: &mut Response<ResponseBody>, req_origin: Option<&str>) {
369		if let Some(cfg) = &self.cors_config {
370			cfg.apply_to_response(resp, req_origin);
371		}
372
373		// Content sniffing turns a mislabelled response into a script-execution vector,
374		// and an API serving user-supplied content has no way to know it is safe.
375		let headers = resp.headers_mut();
376		headers
377			.entry(hyper::header::X_CONTENT_TYPE_OPTIONS)
378			.or_insert(HeaderValue::from_static("nosniff"));
379
380		for (name, value) in self.extra_headers.iter() {
381			headers.entry(name).or_insert(value.clone());
382		}
383	}
384
385	/// `req_origin` is captured once by [`route_with`] and threaded here rather than
386	/// re-read. Two independent lookups of an attacker-controlled header are two chances
387	/// to disagree, and the preflight branch below and `finalize` reflecting different
388	/// values would be a header-smuggling primitive. One read, one value, both users.
389	async fn route_inner(
390		&self,
391		req: Request<Incoming>,
392		peer: Option<SocketAddr>,
393		req_origin: Option<&str>,
394	) -> Response<ResponseBody> {
395		// RFC 9112 §3.2: a server MUST answer 400 to an HTTP/1.1 request with no `Host`.
396		// hyper 1.11 serves these, so the check lives here — a request with no authority
397		// is ambiguous to anything downstream doing name-based routing, and this crate
398		// would otherwise pass that ambiguity along. Absolute-form targets carry the
399		// authority in the URI, which satisfies the requirement, and HTTP/1.0 never had
400		// it, so both are exempt.
401		if req.version() == hyper::Version::HTTP_11
402			&& req.uri().authority().is_none()
403			&& !req.headers().contains_key(hyper::header::HOST)
404		{
405			return (self.error_handler)(StatusCode::BAD_REQUEST, "missing host header");
406		}
407		if req.uri().path().len() > MAX_PATH_LEN {
408			return (self.error_handler)(StatusCode::BAD_REQUEST, "path too long");
409		}
410		if req.uri().query().map(|q| q.len()).unwrap_or(0) > MAX_QUERY_LEN {
411			return (self.error_handler)(StatusCode::BAD_REQUEST, "query string too long");
412		}
413
414		let method = req.method().clone();
415		let path = req.uri().path().to_string();
416		let state = State::from_arc(Arc::clone(&self.state));
417		// A request with no query string built a HashMap to hold nothing. Guarded on
418		// *emptiness*, not absence: `?` alone parses as `Some("")`, so `is_none()`
419		// would miss exactly the case this is here to catch. The `MAX_QUERY_LEN`
420		// bound above stays where it is and stays unconditional — folding it in here
421		// would let an oversized query skip its limit whenever this guard
422		// short-circuits.
423		let query = req.uri().query().unwrap_or("");
424		let query_params = if query.is_empty() {
425			QueryParams::default()
426		} else {
427			parse_query(query)
428		};
429
430		// Handle CORS preflight only for existing routes
431		if method == Method::OPTIONS && req_origin.is_some() {
432			if let Some(cfg) = &self.cors_config {
433				if self.router.path_exists(&path) {
434					let requested_headers = req
435						.headers()
436						.get("access-control-request-headers")
437						.and_then(|v| v.to_str().ok());
438					let allowed = self.allowed_methods_with_head(&path);
439					return cfg.preflight_response(req_origin, requested_headers, &allowed);
440				}
441			}
442		}
443
444		let method_to_match = if method == Method::HEAD {
445			Method::GET
446		} else {
447			method.clone()
448		};
449
450		match self.router.match_route(&method_to_match, &path) {
451			Some((handler, params)) => {
452				let mut req = req;
453				// Each insert boxes its value and hashes a `TypeId`. A route with no
454				// params and a request with no query string pay both for nothing —
455				// `/health` was inserting an empty `PathParams` on every request.
456				// `query_params()` and `path_params()` read absence as emptiness, so
457				// no consumer can tell these were skipped.
458				if !query_params.0.is_empty() {
459					req.extensions_mut().insert(query_params);
460				}
461				if !params.0.is_empty() {
462					req.extensions_mut().insert(params);
463				}
464				// `json_body` falls back to `DEFAULT_MAX_BODY_SIZE` when this is absent,
465				// so an app on the default gets identical behaviour without paying for a
466				// boxed extension on every request.
467				if self.max_body_size != DEFAULT_MAX_BODY_SIZE {
468					req.extensions_mut().insert(MaxBodySize(self.max_body_size));
469				}
470				if let Some(peer) = peer {
471					req.extensions_mut().insert(PeerAddr(peer));
472				}
473				match handler(req, state).await {
474					Ok(resp) => resp,
475					Err(e) => {
476						let status = StatusCode::from_u16(e.code)
477							.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
478						// A 5xx body is sanitized to "internal server error" on purpose —
479						// handler messages can carry internals a client must not see. The
480						// real message goes here instead of being discarded with it, which
481						// is what previously left an operator nothing to debug from. 4xx
482						// messages are already sent to the client, so they are not repeated.
483						if status.is_server_error() {
484							log_line(&self.log, format_args!("{status} {}: {}", path, e.message));
485						}
486						(self.error_handler)(status, &e.message)
487					}
488				}
489			}
490			None => {
491				let allowed = self.allowed_methods_with_head(&path);
492				if !allowed.is_empty() {
493					let mut method_strs: Vec<&str> = allowed.iter().map(|m| m.as_str()).collect();
494					method_strs.sort();
495					method_strs.dedup();
496					let allow_header = method_strs.join(", ");
497					let mut resp = (self.error_handler)(StatusCode::METHOD_NOT_ALLOWED, "method not allowed");
498					if let Ok(val) = allow_header.parse() {
499						resp.headers_mut().insert("allow", val);
500					}
501					resp
502				} else {
503					(self.error_handler)(StatusCode::NOT_FOUND, "not found")
504				}
505			}
506		}
507	}
508
509	/// Every method the router accepts for `path`, plus `HEAD` whenever
510	/// `GET` is one of them (hyper's HTTP/1 server answers `HEAD` by running
511	/// the `GET` handler and discarding the body — see `route()` above —
512	/// so `HEAD` is always implicitly valid alongside `GET`). Shared by the
513	/// CORS preflight branch and the plain 405 branch so the two can never
514	/// disagree about what a path actually accepts.
515	fn allowed_methods_with_head(&self, path: &str) -> Vec<Method> {
516		let mut allowed = self.router.allowed_methods(path);
517		if allowed.contains(&Method::GET) {
518			allowed.push(Method::HEAD);
519		}
520		allowed
521	}
522
523	/// Bind to an ephemeral port and serve in the background.
524	///
525	/// Returns the assigned port number. The server runs in a spawned task
526	/// and serves until the process exits. For graceful shutdown, use `run()`.
527	/// Binds to 127.0.0.1 only—safe for development and testing.
528	pub async fn bind_ephemeral(self) -> Result<u16, ServeError> {
529		let listener = TcpListener::bind(ephemeral_bind_addr())
530			.await
531			.map_err(|e| ServeError::new(500, format!("failed to bind to ephemeral port: {e}")))?;
532		let port = listener
533			.local_addr()
534			.map_err(|e| ServeError::new(500, format!("failed to get assigned port: {e}")))?
535			.port();
536		let app = Arc::new(self);
537		tokio::spawn(async move {
538			serve_loop(listener, app, std::future::pending(), plain_connect).await;
539		});
540		Ok(port)
541	}
542
543	/// Bind to an ephemeral port with TLS and serve in the background.
544	///
545	/// Requires the `tls` feature. Returns the assigned port number.
546	/// The server runs in a spawned task and enforces TLS handshake timeouts
547	/// to prevent stalled clients from blocking the accept loop.
548	#[cfg(feature = "tls")]
549	pub async fn bind_tls_ephemeral(
550		self,
551		config: Arc<rustls::ServerConfig>,
552	) -> Result<u16, ServeError> {
553		let listener = TcpListener::bind(ephemeral_bind_addr())
554			.await
555			.map_err(|e| ServeError::new(500, format!("failed to bind to ephemeral port: {e}")))?;
556		let port = listener
557			.local_addr()
558			.map_err(|e| ServeError::new(500, format!("failed to get assigned port: {e}")))?
559			.port();
560		let handshake_timeout = self.tls_handshake_timeout;
561		let acceptor = TlsAcceptor::from(config);
562		let app = Arc::new(self);
563		tokio::spawn(async move {
564			serve_loop(listener, app, std::future::pending(), tls_connect(acceptor, handshake_timeout)).await;
565		});
566		Ok(port)
567	}
568
569	/// Serve `listener` until `shutdown` resolves, then drain in-flight
570	/// connections and return. The production entry point for callers that
571	/// want control over the shutdown trigger (tests, custom signals); see
572	/// [`App::bind`] for the OS-signal convenience wrapper.
573	pub async fn run<F>(self, listener: TcpListener, shutdown: F) -> Result<(), ServeError>
574	where
575		F: Future<Output = ()> + Send + 'static,
576	{
577		let app = Arc::new(self);
578		serve_loop(listener, app, shutdown, plain_connect).await;
579		Ok(())
580	}
581
582	/// Bind `addr` and serve until SIGINT or SIGTERM, then drain in-flight
583	/// connections and return. Unlike [`App::bind_ephemeral`], `addr` is
584	/// caller-chosen — e.g. `0.0.0.0:$PORT` for a platform like fly.io that
585	/// routes external traffic to the process directly.
586	pub async fn bind(self, addr: SocketAddr) -> Result<(), ServeError> {
587		let shutdown = signal_shutdown()?;
588		let listener = TcpListener::bind(addr)
589			.await
590			.map_err(|e| ServeError::new(500, format!("failed to bind to {addr}: {e}")))?;
591		self.run(listener, shutdown).await
592	}
593
594	/// TLS variant of [`App::run`].
595	#[cfg(feature = "tls")]
596	pub async fn run_tls<F>(
597		self,
598		listener: TcpListener,
599		config: Arc<rustls::ServerConfig>,
600		shutdown: F,
601	) -> Result<(), ServeError>
602	where
603		F: Future<Output = ()> + Send + 'static,
604	{
605		let handshake_timeout = self.tls_handshake_timeout;
606		let acceptor = TlsAcceptor::from(config);
607		let app = Arc::new(self);
608		serve_loop(listener, app, shutdown, tls_connect(acceptor, handshake_timeout)).await;
609		Ok(())
610	}
611
612	/// TLS variant of [`App::bind`].
613	#[cfg(feature = "tls")]
614	pub async fn bind_tls(
615		self,
616		addr: SocketAddr,
617		config: Arc<rustls::ServerConfig>,
618	) -> Result<(), ServeError> {
619		let shutdown = signal_shutdown()?;
620		let listener = TcpListener::bind(addr)
621			.await
622			.map_err(|e| ServeError::new(500, format!("failed to bind to {addr}: {e}")))?;
623		self.run_tls(listener, config, shutdown).await
624	}
625}
626
627impl App<()> {
628	pub fn stateless() -> Self {
629		App::new(())
630	}
631}
632
633/// Wires an accepted (and, for TLS, already-handshaken) connection up to the
634/// hyper HTTP/1 service and drives it to completion. Shared by every accept
635/// loop below so the framing/timeout setup is defined exactly once.
636async fn serve_connection<S, IO>(io: IO, app: Arc<App<S>>, header_read_timeout: Duration, peer: SocketAddr)
637where
638	S: Send + Sync + 'static,
639	IO: hyper::rt::Read + hyper::rt::Write + Unpin + Send + 'static,
640{
641	let app_for_conn = app.clone();
642	let log_for_conn = app.log.clone();
643
644	// Where a handler's upgrade callback is parked between being returned and the
645	// connection being released. It cannot run inside the service call: the `101` has not
646	// been written yet at that point, so hyper's upgrade future cannot resolve and awaiting
647	// it there would deadlock the connection it is waiting on.
648	let pending_upgrade: Arc<Mutex<Option<(hyper::upgrade::OnUpgrade, OnUpgrade)>>> =
649		Arc::new(Mutex::new(None));
650	let pending_for_service = pending_upgrade.clone();
651
652	let svc = service_fn(move |mut req: Request<Incoming>| {
653		let app = app.clone();
654		let pending = pending_for_service.clone();
655		// Taken before routing, because routing consumes the request. This is why a
656		// handler cannot call `hyper::upgrade::on` itself — documented on `with_upgrades`.
657		let upgrade = hyper::upgrade::on(&mut req);
658		async move {
659			// Pay for the log line only when there is a sink.
660			let observed = app.log.as_ref().map(|_| {
661				(
662					std::time::Instant::now(),
663					req.method().clone(),
664					req.uri().path().to_string(),
665				)
666			});
667
668			let resp = app.route_with_peer(req, peer).await;
669
670			if let Some((started, method, path)) = observed {
671				log_line(
672					&app.log,
673					format_args!(
674						"{method} {path} {} {:.3}ms",
675						resp.status().as_u16(),
676						started.elapsed().as_secs_f64() * 1000.0
677					),
678				);
679			}
680			let mut resp = resp;
681			if let Some(callback) = resp.extensions_mut().remove::<OnUpgrade>() {
682				if app.upgrades_enabled {
683					*pending.lock().unwrap() = Some((upgrade, callback));
684				} else {
685					// Silence here would leave the client holding a dead connection and
686					// the handler author with nothing to go on.
687					log_line(
688						&app.log,
689						format_args!(
690							"handler returned an upgrade for {} but the app was not built \
691							 with with_upgrades(); the connection will not be upgraded",
692							resp.status().as_u16()
693						),
694					);
695				}
696			}
697			Ok::<_, hyper::Error>(resp)
698		}
699	});
700	let mut builder = http1::Builder::new();
701	builder.timer(TokioTimer::new());
702	builder.header_read_timeout(header_read_timeout);
703
704	if app_for_conn.upgrades_enabled {
705		let _ = builder.serve_connection(io, svc).with_upgrades().await;
706	} else {
707		let _ = builder.serve_connection(io, svc).await;
708	}
709
710	// The connection is released; if a handler asked to take it over, service the upgraded
711	// stream here — still inside this task, which holds the semaphore permit and is the one
712	// the shutdown drain aborts. A detached `tokio::spawn` here would escape both.
713	let taken = pending_upgrade.lock().unwrap().take();
714	if let Some((upgrade, callback)) = taken {
715		match tokio::time::timeout(UPGRADE_HANDOFF_TIMEOUT, upgrade).await {
716			Ok(Ok(upgraded)) => callback.run(TokioIo::new(upgraded)).await,
717			Ok(Err(e)) => log_line(&log_for_conn, format_args!("upgrade failed: {e}")),
718			Err(_) => log_line(
719				&log_for_conn,
720				format_args!(
721					"upgrade did not complete within {UPGRADE_HANDOFF_TIMEOUT:?}; \
722					 releasing the connection"
723				),
724			),
725		}
726	}
727}
728
729async fn plain_connect(stream: TcpStream) -> Option<TcpStream> {
730	Some(stream)
731}
732
733/// Each accepted TCP connection must complete the TLS handshake within
734/// `handshake_timeout`; a stalled or malicious client that never sends a
735/// ClientHello is dropped without blocking the accept loop from serving
736/// other connections.
737#[cfg(feature = "tls")]
738fn tls_connect(
739	acceptor: TlsAcceptor,
740	handshake_timeout: Duration,
741) -> impl Fn(TcpStream) -> Pin<Box<dyn Future<Output = Option<tokio_rustls::server::TlsStream<TcpStream>>> + Send>> + Clone {
742	move |stream| {
743		let acceptor = acceptor.clone();
744		Box::pin(async move {
745			match tokio::time::timeout(handshake_timeout, acceptor.accept(stream)).await {
746				Ok(Ok(s)) => Some(s),
747				Ok(Err(_)) | Err(_) => None,
748			}
749		})
750	}
751}
752
753/// Accept a connection and reserve it a connection-limit permit, retrying
754/// transient `accept()` errors with [`Backoff`]. Returns `None` only if the
755/// semaphore itself has been closed (never happens in normal operation, since
756/// nothing ever calls `close()` on it — handled so a caller can still fail
757/// safely rather than panic).
758///
759/// Deliberately returns one future that covers accept *and* permit
760/// acquisition, so a caller can race the whole thing against a shutdown
761/// signal in a single `select!`. Racing only the accept and leaving permit
762/// acquisition as a bare `.await` afterward was the prior implementation's
763/// bug: once a connection was accepted but was waiting on a saturated
764/// semaphore, that wait was invisible to the `select!` and shutdown could not
765/// preempt it.
766async fn accept_and_permit<L: TcpAccept>(
767	listener: &L,
768	backoff: &mut Backoff,
769	semaphore: &Arc<tokio::sync::Semaphore>,
770) -> Option<(TcpStream, SocketAddr, tokio::sync::OwnedSemaphorePermit)> {
771	// The permit is taken *before* accepting, and the order is load-bearing rather
772	// than stylistic. This whole future is dropped whenever another arm of the
773	// caller's `select!` wins — a finished connection being reaped, or shutdown — and
774	// whatever it is holding at that moment is destroyed with it. A permit is free to
775	// lose and re-acquire; an accepted `TcpStream` is a client's connection, and
776	// dropping one mid-wait resets it. Accepting second also puts backpressure where
777	// it belongs: at capacity the server stops accepting, so waiting clients sit in
778	// the kernel's backlog instead of in a userspace queue that a cancellation can
779	// silently empty.
780	let permit = semaphore.clone().acquire_owned().await.ok()?;
781
782	loop {
783		match listener.accept().await {
784			Ok((stream, peer)) => {
785				backoff.reset();
786				return Some((stream, peer, permit));
787			}
788			Err(_) => tokio::time::sleep(backoff.next_delay()).await,
789		}
790	}
791}
792
793/// Describe a failure to install a signal handler.
794///
795/// Split out so the mapping is testable: the failure itself needs a
796/// signal-handler-hostile environment and cannot be provoked in-process, so the
797/// shape of what a caller would receive is what gets asserted.
798fn signal_install_error(signal: &str, cause: std::io::Error) -> ServeError {
799	ServeError::new(500, format!("failed to install {signal} handler: {cause}"))
800}
801
802/// Set up a shutdown future that fires on SIGINT or SIGTERM.
803///
804/// Both handlers are installed eagerly, before the returned future is awaited,
805/// so a server that cannot hear a shutdown signal fails at startup rather than
806/// binding a port and then silently ignoring SIGTERM forever.
807fn signal_shutdown() -> Result<impl Future<Output = ()> + Send, ServeError> {
808	let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
809		.map_err(|e| signal_install_error("SIGINT", e))?;
810	let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
811		.map_err(|e| signal_install_error("SIGTERM", e))?;
812
813	Ok(async move {
814		tokio::select! {
815			_ = sigint.recv() => {}
816			_ = sigterm.recv() => {}
817		}
818	})
819}
820
821/// Accept loop shared by every bind/run entry point (plain and TLS, with and
822/// without graceful shutdown). `connect` turns a raw `TcpStream` into the
823/// transport handed to `serve_connection` — identity for plaintext
824/// ([`plain_connect`]), a timed handshake for TLS ([`tls_connect`]). A caller
825/// that never needs graceful shutdown (`bind_ephemeral`, `bind_tls_ephemeral`)
826/// passes `std::future::pending()`, which can never resolve, so this loop
827/// degenerates into a plain accept-forever loop for them.
828///
829/// The accept-and-permit step and the shutdown signal are the two arms of a
830/// single `select!`, so shutdown can win the race — and cancel a pending
831/// accept or a permit wait cleanly — at any point, not just between
832/// iterations.
833async fn serve_loop<S, F, C, Fut, IO>(listener: TcpListener, app: Arc<App<S>>, shutdown: F, connect: C)
834where
835	S: Send + Sync + 'static,
836	F: Future<Output = ()> + Send + 'static,
837	C: Fn(TcpStream) -> Fut + Send + Sync + 'static,
838	Fut: Future<Output = Option<IO>> + Send + 'static,
839	IO: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
840{
841	let header_read_timeout = app.header_read_timeout;
842	let semaphore = Arc::new(tokio::sync::Semaphore::new(app.max_connections));
843	let connect = Arc::new(connect);
844	let mut backoff = Backoff::new();
845	let mut join_set: tokio::task::JoinSet<()> = tokio::task::JoinSet::new();
846	let mut shutdown_pin = std::pin::pin!(shutdown);
847	let mut shutting_down = false;
848
849	loop {
850		if !shutting_down {
851			tokio::select! {
852				accepted = accept_and_permit(&listener, &mut backoff, &semaphore) => {
853					match accepted {
854						Some((stream, peer, permit)) => {
855							let app = app.clone();
856							let connect = connect.clone();
857							join_set.spawn(async move {
858								let _permit = permit;
859								if let Some(io) = connect(stream).await {
860									serve_connection(TokioIo::new(io), app, header_read_timeout, peer).await;
861								}
862							});
863						}
864						None => shutting_down = true,
865					}
866				}
867				// Reaping finished connections here — rather than only at shutdown — is
868				// what makes a handler panic visible while the server is still running.
869				// `join_next` on an empty set returns immediately with `None`, which
870				// would busy-spin this arm, so it is only polled when work is in flight.
871				joined = join_set.join_next(), if !join_set.is_empty() => {
872					if let Some(joined) = joined {
873						report_if_panicked(&app.log, joined);
874					}
875				}
876				_ = shutdown_pin.as_mut() => {
877					shutting_down = true;
878				}
879			}
880			continue;
881		}
882
883		// Shutting down: drain what is in flight, but only for so long. A handler that
884		// never returns — or a streaming response with no natural end, like a
885		// server-sent-event stream — would otherwise hold shutdown open indefinitely.
886		// Connections still running when the grace period expires are aborted, which the
887		// client observes as a dropped connection: the correct outcome for a server that
888		// was asked to stop and said it would.
889		let drained = tokio::time::timeout(DEFAULT_SHUTDOWN_DRAIN_TIMEOUT, async {
890			while let Some(joined) = join_set.join_next().await {
891				report_if_panicked(&app.log, joined);
892			}
893		})
894		.await;
895
896		if drained.is_err() {
897			join_set.shutdown().await;
898		}
899		break;
900	}
901}
902
903/// Builder for configuring routes and settings before creating an `App`.
904///
905/// `RouteBuilder` uses a fluent API to register routes, configure CORS, and adjust
906/// server settings. Call `.seal()` to produce the final `App<S>`.
907///
908/// # Example
909///
910/// ```ignore
911/// use mini_serve::{RouteBuilder, handler, body};
912/// use hyper::Response;
913/// use hyper::body::Bytes;
914///
915/// #[tokio::main]
916/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
917///     let app = RouteBuilder::stateless()
918///         .get("/health", handler(|_, _| async {
919///             Ok(Response::new(body(Bytes::from("OK"))))
920///         }))
921///         .seal();
922///
923///     app.bind("127.0.0.1:8080".parse()?).await?;
924///     Ok(())
925/// }
926/// ```
927#[must_use = "RouteBuilder does nothing until .seal() is called"]
928pub struct RouteBuilder<S> {
929	state:               Arc<S>,
930	router:              Router<S>,
931	max_body_size:       usize,
932	header_read_timeout: Duration,
933	log:                 Option<LogSink>,
934	extra_headers:       Arc<Vec<(HeaderName, HeaderValue)>>,
935	max_connections:     usize,
936	#[cfg(feature = "tls")]
937	tls_handshake_timeout: Duration,
938	error_handler:       ErrorHandler,
939	cors_config:         Option<CorsConfig>,
940	middlewares:         Vec<Middleware<S>>,
941	upgrades_enabled:    bool,
942}
943
944impl<S: Send + Sync + 'static> RouteBuilder<S> {
945	/// Create a new builder with shared state.
946	pub fn new(state: S) -> Self {
947		RouteBuilder {
948			state:               Arc::new(state),
949			router:              Router::new(),
950			max_body_size:       DEFAULT_MAX_BODY_SIZE,
951			header_read_timeout: DEFAULT_HEADER_READ_TIMEOUT,
952			upgrades_enabled:    false,
953			log:                 None,
954			extra_headers:       Arc::new(Vec::new()),
955			max_connections:     DEFAULT_MAX_CONNECTIONS,
956			#[cfg(feature = "tls")]
957			tls_handshake_timeout: DEFAULT_TLS_HANDSHAKE_TIMEOUT,
958			error_handler:       default_error_handler(),
959			cors_config:         None,
960			middlewares:         Vec::new(),
961		}
962	}
963
964	/// Register a middleware. Applies to every route registered *after* this
965	/// call — middlewares wrap in registration order, so the first `.wrap()`
966	/// call becomes the outermost layer and runs first.
967	pub fn wrap(mut self, middleware: Middleware<S>) -> Self {
968		self.middlewares.push(middleware);
969		self
970	}
971
972	fn apply_middlewares(&self, handler: Handler<S>) -> Handler<S> {
973		self.middlewares.iter().rev().fold(handler, |acc, mw| mw(acc))
974	}
975
976	/// Set the maximum request body size in bytes (default: 2 MiB).
977	pub fn with_max_body_size(mut self, max: usize) -> Self {
978		self.max_body_size = max;
979		self
980	}
981
982	/// Set the header read timeout (default: 30 seconds).
983	pub fn with_header_read_timeout(mut self, d: Duration) -> Self {
984		self.header_read_timeout = d;
985		self
986	}
987
988	/// Log one line per request to stderr, plus handler panics and the internal detail
989	/// behind 5xx responses.
990	///
991	/// Off by default: a library writing to its host's stderr uninvited is a surprise.
992	/// See [`RouteBuilder::with_request_logging_to`] to choose the destination.
993	/// Send `name: value` on every response that does not already carry that header.
994	///
995	/// For the policy headers an API wants applied uniformly — `Strict-Transport-Security`,
996	/// `Content-Security-Policy`, `Referrer-Policy`.
997	///
998	/// **Apply-if-absent**, deliberately: a handler that sets the header itself wins.
999	/// This is the opposite of `mini-static`'s rule, and for the opposite reason — that
1000	/// crate computes every header itself, so a fixed value fighting a computed one is a
1001	/// bug, whereas handlers here are arbitrary user code that may legitimately vary a
1002	/// policy per route.
1003	///
1004	/// # Errors
1005	///
1006	/// [`ServeError`] if `name` or `value` is not a valid HTTP header, or if `name` is
1007	/// one the connection layer owns (`Content-Length`, `Connection`,
1008	/// `Transfer-Encoding`) — those describe framing this crate does not choose, so a
1009	/// fixed value could only contradict it.
1010	pub fn with_response_header(mut self, name: &str, value: &str) -> Result<Self, ServeError> {
1011		let name = HeaderName::from_bytes(name.as_bytes())
1012			.map_err(|_| ServeError::new(500, format!("invalid header name: {name}")))?;
1013		let value = HeaderValue::from_str(value)
1014			.map_err(|_| ServeError::new(500, format!("invalid value for header {name}")))?;
1015
1016		if CONNECTION_OWNED_HEADERS.contains(&name) {
1017			return Err(ServeError::new(
1018				500,
1019				format!("{name} is owned by the connection layer and cannot be set as a fixed header"),
1020			));
1021		}
1022
1023		Arc::make_mut(&mut self.extra_headers).push((name, value));
1024		Ok(self)
1025	}
1026
1027	pub fn with_request_logging(self) -> Self {
1028		self.with_request_logging_to(Box::new(std::io::stderr()))
1029	}
1030
1031	/// Log one line per request to `writer`, plus handler panics and 5xx internals.
1032	///
1033	/// Each served request writes `GET /path 200 0.421ms` — method, path exactly as
1034	/// received, status, and handling time. Two failures that are otherwise invisible
1035	/// also come here:
1036	///
1037	/// - **Handler panics.** The task's result was previously discarded, so a panicking
1038	///   handler dropped the client's connection and left no trace anywhere.
1039	/// - **5xx internal detail.** The client body is sanitized to
1040	///   `{"message":"internal server error"}` deliberately; without a sink the real
1041	///   message was discarded with it, leaving an operator nothing to debug from.
1042	///
1043	/// The path is logged exactly as received, not decoded: it is attacker-controlled
1044	/// input, and whoever reads the log deserves the bytes that actually arrived.
1045	/// Writes are serialized across connections; write failures are ignored rather than
1046	/// allowed to fail a request.
1047	pub fn with_request_logging_to(mut self, writer: Box<dyn std::io::Write + Send>) -> Self {
1048		self.log = Some(Arc::new(Mutex::new(writer)));
1049		self
1050	}
1051
1052	/// Set the TLS handshake timeout (default: 10 seconds).
1053	/// Requires the `tls` feature.
1054	#[cfg(feature = "tls")]
1055	pub fn with_tls_handshake_timeout(mut self, d: Duration) -> Self {
1056		self.tls_handshake_timeout = d;
1057		self
1058	}
1059
1060	/// Set the maximum concurrent connections (default: 1024).
1061	/// Allow handlers to take over a connection with [`OnUpgrade`].
1062	///
1063	/// Off by default: an application with no upgrade route should not pay for the
1064	/// capability, and enabling it changes how every connection on this server is served.
1065	/// Without it, a `101` response is written and the callback never runs — which is
1066	/// reported to the log sink if one is configured, since the client would otherwise see
1067	/// a dead connection and the author would see nothing.
1068	///
1069	/// The server takes the request's upgrade future before routing, so a handler that
1070	/// calls `hyper::upgrade::on` itself receives nothing. Use [`OnUpgrade`] instead; it
1071	/// exists so the upgraded stream is serviced inside the connection's own task, keeping
1072	/// it inside the connection ceiling and the shutdown drain.
1073	pub fn with_upgrades(mut self) -> Self {
1074		self.upgrades_enabled = true;
1075		self
1076	}
1077
1078	pub fn with_max_connections(mut self, max: usize) -> Self {
1079		self.max_connections = max;
1080		self
1081	}
1082
1083	pub fn with_error_handler(
1084		mut self,
1085		f: impl Fn(StatusCode, &str) -> Response<ResponseBody> + Send + Sync + 'static,
1086	) -> Self {
1087		self.error_handler = Arc::new(f);
1088		self
1089	}
1090
1091	pub fn with_cors(mut self, config: CorsConfig) -> Self {
1092		self.cors_config = Some(config);
1093		self
1094	}
1095
1096	pub fn get(mut self, path: &str, handler: Handler<S>) -> Self {
1097		let handler = self.apply_middlewares(handler);
1098		self.router.insert(Method::GET, path, handler);
1099		self
1100	}
1101
1102	pub fn post(mut self, path: &str, handler: Handler<S>) -> Self {
1103		let handler = self.apply_middlewares(handler);
1104		self.router.insert(Method::POST, path, handler);
1105		self
1106	}
1107
1108	pub fn put(mut self, path: &str, handler: Handler<S>) -> Self {
1109		let handler = self.apply_middlewares(handler);
1110		self.router.insert(Method::PUT, path, handler);
1111		self
1112	}
1113
1114	pub fn delete(mut self, path: &str, handler: Handler<S>) -> Self {
1115		let handler = self.apply_middlewares(handler);
1116		self.router.insert(Method::DELETE, path, handler);
1117		self
1118	}
1119
1120	/// Register a `PATCH` handler.
1121	///
1122	/// Dispatch and the `Allow` header are generic over `Method`, so this needs nothing
1123	/// from the router that its siblings do not — it was simply missing.
1124	pub fn patch(mut self, path: &str, handler: Handler<S>) -> Self {
1125		let handler = self.apply_middlewares(handler);
1126		self.router.insert(Method::PATCH, path, handler);
1127		self
1128	}
1129
1130	pub fn seal(self) -> App<S> {
1131		App {
1132			state:              self.state,
1133			router:             Arc::new(self.router),
1134			max_body_size:       self.max_body_size,
1135			header_read_timeout: self.header_read_timeout,
1136			upgrades_enabled:    self.upgrades_enabled,
1137			log:                 self.log,
1138			extra_headers:       self.extra_headers,
1139			max_connections:     self.max_connections,
1140			#[cfg(feature = "tls")]
1141			tls_handshake_timeout: self.tls_handshake_timeout,
1142			error_handler:       self.error_handler,
1143			cors_config:         self.cors_config,
1144		}
1145	}
1146}
1147
1148impl RouteBuilder<()> {
1149	pub fn stateless() -> Self {
1150		RouteBuilder::new(())
1151	}
1152}
1153
1154#[cfg(test)]
1155#[path = "../tests/unit/app.rs"]
1156mod tests;