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;