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