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;