Skip to main content

mini_serve/
app.rs

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