mini_static/server.rs
1use std::convert::Infallible;
2use std::fs;
3use std::io::Read;
4use std::net::SocketAddr;
5use std::path::{Path, PathBuf};
6use std::pin::Pin;
7use std::sync::Arc;
8use std::task::{Context, Poll};
9use std::time::{Duration, SystemTime};
10
11use bytes::Bytes;
12use hyper::{Method, Response, StatusCode, Request};
13use hyper::service::service_fn;
14use http_body_util::Full;
15use hyper::body::Incoming;
16use hyper_util::rt::TokioExecutor;
17use hyper_util::rt::TokioIo;
18use hyper_util::server::conn::auto::Builder as AutoBuilder;
19use tokio::fs::File;
20use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, ReadBuf};
21use tokio::net::{TcpListener, TcpStream};
22use tokio::sync::{OwnedSemaphorePermit, Semaphore};
23use tokio::time::timeout;
24
25use crate::error::StaticError;
26use crate::handler::{FileBody, ResponseBody};
27use crate::reload::{self, SseBody};
28use crate::resolve;
29use crate::watcher::{start_watching, Broadcaster};
30
31const ACCEPT_BACKOFF_INITIAL: Duration = Duration::from_millis(10);
32const ACCEPT_BACKOFF_MAX: Duration = Duration::from_secs(1);
33
34/// A source of accepted TCP connections. Abstracted so the accept-error backoff below
35/// can be exercised against a listener that fails on demand, without needing to provoke
36/// real OS-level accept errors (e.g. EMFILE) in tests.
37trait TcpAccept {
38 async fn accept(&self) -> std::io::Result<(TcpStream, SocketAddr)>;
39}
40
41impl TcpAccept for TcpListener {
42 async fn accept(&self) -> std::io::Result<(TcpStream, SocketAddr)> {
43 TcpListener::accept(self).await
44 }
45}
46
47/// Exponential backoff for retrying `accept()` after an error, so a sustained failure
48/// (e.g. the process is out of file descriptors) degrades into periodic retries instead
49/// of a CPU-bound busy spin or, worse, silently ending the accept loop for good. Resets
50/// to the initial delay as soon as an accept succeeds.
51struct Backoff {
52 delay: Duration,
53}
54
55impl Backoff {
56 fn new() -> Self {
57 Backoff { delay: ACCEPT_BACKOFF_INITIAL }
58 }
59
60 fn next_delay(&mut self) -> Duration {
61 let delay = self.delay;
62 self.delay = (self.delay * 2).min(ACCEPT_BACKOFF_MAX);
63 delay
64 }
65
66 fn reset(&mut self) {
67 self.delay = ACCEPT_BACKOFF_INITIAL;
68 }
69}
70
71/// Accept a connection and reserve it a connection-limit permit, retrying transient
72/// `accept()` errors with `Backoff` instead of ending the accept loop on the first one.
73/// Returns `None` only if the semaphore itself has been closed (never happens in normal
74/// operation, since nothing ever calls `close()` on it — handled so a caller can still
75/// fail safely rather than panic).
76async fn accept_and_permit<L: TcpAccept>(
77 listener: &L,
78 backoff: &mut Backoff,
79 semaphore: &Arc<Semaphore>,
80) -> Option<(TcpStream, OwnedSemaphorePermit)> {
81 loop {
82 let stream = match listener.accept().await {
83 Ok((stream, _)) => {
84 backoff.reset();
85 stream
86 }
87 Err(_) => {
88 tokio::time::sleep(backoff.next_delay()).await;
89 continue;
90 }
91 };
92 return match semaphore.clone().acquire_owned().await {
93 Ok(permit) => Some((stream, permit)),
94 Err(_) => None,
95 };
96 }
97}
98
99/// A static file server for serving files securely from a root directory.
100///
101/// `Server` canonicalizes the root directory once at creation time and uses the
102/// canonical form for all subsequent requests, avoiding repeated filesystem calls.
103///
104/// # Security
105///
106/// The server protects against:
107/// - Path traversal attacks (e.g., `../../etc/passwd`)
108/// - Accessing files outside the root via symlinks
109/// - Disclosing filesystem structure (traversal and missing files both return 404)
110///
111/// # Cloning
112///
113/// `Server` is cheap to clone (a `PathBuf` and a `usize`). Multiple clones can be used
114/// concurrently in async tasks without synchronization overhead.
115///
116/// # Example
117///
118/// ```no_run
119/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
120/// use mini_static::Server;
121/// use std::path::Path;
122/// use std::time::Duration;
123///
124/// let server = Server::new(Path::new("./public"))?;
125/// let (port, _handle) = server.run(Duration::from_secs(30)).await?;
126/// println!("Server running on port {}", port);
127/// # Ok(())
128/// # }
129/// ```
130#[derive(Clone)]
131pub struct Server {
132 root_canon: PathBuf,
133 max_connections: usize,
134 live_reload: bool,
135 broadcaster: Option<Broadcaster>,
136}
137
138impl Server {
139 /// Create a new server with the given root directory.
140 ///
141 /// Canonicalizes the root once at startup. All subsequent requests use the
142 /// canonical root without re-canonicalizing it, making this suitable for long-lived servers.
143 ///
144 /// # Arguments
145 ///
146 /// * `root` - The root directory to serve files from.
147 ///
148 /// # Errors
149 ///
150 /// Returns `Err(StaticError::Io)` if the root cannot be canonicalized (e.g., doesn't exist,
151 /// no read permissions).
152 pub fn new(root: &Path) -> Result<Self, StaticError> {
153 let root_canon = root.canonicalize().map_err(StaticError::Io)?;
154 Ok(Server {
155 root_canon,
156 max_connections: DEFAULT_MAX_CONNECTIONS,
157 live_reload: false,
158 broadcaster: None,
159 })
160 }
161
162 /// Set the maximum number of connections served concurrently (default 1024).
163 ///
164 /// Once this many connections are in flight, `run()`'s accept loop stops accepting
165 /// new ones — without pausing the accept loop, a client that opens a connection and
166 /// sends nothing (see the header-read timeout docs on `run()`) could otherwise be
167 /// used, in enough parallel copies, to exhaust the process's file descriptors or
168 /// memory with no bound at all.
169 pub fn with_max_connections(mut self, max: usize) -> Self {
170 self.max_connections = max;
171 self
172 }
173
174 /// Enable live-reload for this server (disabled by default).
175 ///
176 /// Once enabled, `run()`/`run_all()`/`run_on()` start a background watcher (mtime
177 /// polling, bounded 500ms interval — see [`crate::start_watching`]) over the
178 /// server's root the first time the server actually starts accepting connections,
179 /// and:
180 ///
181 /// - serve a live-reload SSE stream at [`crate::reload::LIVE_RELOAD_PATH`],
182 /// broadcasting a change event (with [`crate::ChangeType`]) whenever a served file
183 /// is added, modified, or removed;
184 /// - inject a small `<script>` into every served `text/html` response that connects
185 /// to that stream and reloads the page (or hot-swaps stylesheet `<link>`s, for CSS
186 /// changes) — no manual client wiring required.
187 ///
188 /// This is meant for local development, not production: leave it disabled (the
189 /// default) for any server serving real traffic. A typical call site gates it behind
190 /// `#[cfg(debug_assertions)]` so a release build never pays for the watcher or the
191 /// injected script.
192 ///
193 /// # Example
194 ///
195 /// ```no_run
196 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
197 /// use mini_static::Server;
198 /// use std::path::Path;
199 ///
200 /// let server = Server::new(Path::new("./public"))?;
201 /// #[cfg(debug_assertions)]
202 /// let server = server.with_live_reload();
203 /// # Ok(())
204 /// # }
205 /// ```
206 pub fn with_live_reload(mut self) -> Self {
207 self.live_reload = true;
208 self
209 }
210
211 /// Resolve a request path under the server's root.
212 ///
213 /// This is a lower-level API for resolving paths without generating HTTP responses.
214 /// For most use cases, prefer `handle_request_with_method()` or the `run()` methods.
215 ///
216 /// # Arguments
217 ///
218 /// * `request_path` - The HTTP request path (e.g., `/path/to/file.html`).
219 ///
220 /// # Returns
221 ///
222 /// - `Ok(PathBuf)` if the path resolves to a file within root.
223 /// - `Err(StaticError)` if the path is invalid, missing, or attempts traversal.
224 pub fn resolve(&self, request_path: &str) -> Result<PathBuf, StaticError> {
225 resolve::resolve_with_canonical_root(&self.root_canon, request_path)
226 }
227
228 /// Handle an HTTP GET request for a resource path.
229 ///
230 /// Convenience method equivalent to `handle_request_with_method(&Method::GET, request_path)`.
231 ///
232 /// # Arguments
233 ///
234 /// * `request_path` - The HTTP request path (e.g., `/index.html`).
235 pub fn handle_request(&self, request_path: &str) -> Response<ResponseBody> {
236 self.handle_request_with_method(&Method::GET, request_path)
237 }
238
239 /// Handle an HTTP request with an explicit method.
240 ///
241 /// Only GET and HEAD methods are allowed. Other methods return 405 Method Not Allowed
242 /// with an Allow header listing the permitted methods.
243 ///
244 /// # Arguments
245 ///
246 /// * `method` - The HTTP method (GET and HEAD are allowed; others return 405).
247 /// * `request_path` - The HTTP request path (e.g., `/index.html`).
248 pub fn handle_request_with_method(
249 &self,
250 method: &Method,
251 request_path: &str,
252 ) -> Response<ResponseBody> {
253 self.handle_request_with_headers(method, request_path, None, None)
254 }
255
256 /// Run the server on a specific address with a configurable header-read timeout.
257 ///
258 /// Spawns the server in a background Tokio task and returns immediately with the
259 /// assigned port number and a [`ServerHandle`]. Call `handle.shutdown().await` to
260 /// stop accepting new connections and wait for in-flight connections to finish.
261 ///
262 /// # Header-Read Timeout
263 ///
264 /// Connections that don't send complete HTTP headers within `header_timeout` are closed.
265 /// This prevents slowloris attacks and resource exhaustion from incomplete requests. The
266 /// timeout applies only to the header-read phase — once a complete header block has been
267 /// read, the connection is handed off with no further time bound, so long-lived response
268 /// bodies (e.g. the live-reload SSE stream from [`Server::with_live_reload`]) are not cut
269 /// off mid-stream.
270 ///
271 /// # Arguments
272 ///
273 /// * `addr` - Socket address to bind to (e.g., `127.0.0.1:0` for loopback ephemeral,
274 /// or `0.0.0.0:8080` to bind all interfaces on a fixed port).
275 /// * `header_timeout` - Maximum time to wait for complete HTTP headers on each connection.
276 ///
277 /// # Returns
278 ///
279 /// - `Ok((u16, ServerHandle))` with the assigned port number and a handle for graceful shutdown.
280 /// - `Err(StaticError::Io)` if binding to the socket fails.
281 pub async fn run_on(&self, addr: SocketAddr, header_timeout: Duration) -> Result<(u16, ServerHandle), StaticError> {
282 let listener = TcpListener::bind(addr)
283 .await
284 .map_err(StaticError::Io)?;
285 let port = listener
286 .local_addr()
287 .map_err(StaticError::Io)?
288 .port();
289
290 let mut server = self.clone();
291 if server.live_reload {
292 let broadcaster = Broadcaster::new();
293 start_watching(Arc::new(server.root_canon.clone()), broadcaster.clone());
294 server.broadcaster = Some(broadcaster);
295 }
296 let semaphore = Arc::new(Semaphore::new(server.max_connections));
297 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
298
299 let accept_task = tokio::spawn(async move {
300 let mut backoff = Backoff::new();
301 let mut join_set: tokio::task::JoinSet<()> = tokio::task::JoinSet::new();
302 let mut shutdown_pin = std::pin::pin!(shutdown_rx);
303 let mut shutting_down = false;
304
305 loop {
306 if !shutting_down {
307 // The accept-and-permit step and the shutdown signal race in a single
308 // `select!` so shutdown can preempt a pending accept or a permit wait
309 // cleanly, at any point — not just between loop iterations.
310 tokio::select! {
311 accepted = accept_and_permit(&listener, &mut backoff, &semaphore) => {
312 match accepted {
313 Some((stream, permit)) => {
314 let server = server.clone();
315 join_set.spawn(async move {
316 let _permit = permit;
317 serve_connection(stream, server, header_timeout).await;
318 });
319 }
320 None => shutting_down = true,
321 }
322 }
323 _ = shutdown_pin.as_mut() => {
324 shutting_down = true;
325 }
326 }
327 continue;
328 }
329
330 // Stop accepting; drain already-spawned connections before returning.
331 match join_set.join_next().await {
332 Some(_) => continue,
333 None => break,
334 }
335 }
336 });
337
338 Ok((port, ServerHandle { shutdown_tx: Some(shutdown_tx), accept_task }))
339 }
340
341 /// Run the server on loopback (127.0.0.1) with a configurable header-read timeout.
342 ///
343 /// Binds to an ephemeral port and spawns the server in a background Tokio task.
344 /// Returns immediately with the assigned port number and a [`ServerHandle`]. Dropping
345 /// the handle without calling `shutdown()` leaves the server running in the
346 /// background for the life of the process — the same behavior `run()` always had.
347 /// Call `handle.shutdown().await` to stop accepting new connections and wait for
348 /// in-flight connections to finish.
349 ///
350 /// # Header-Read Timeout
351 ///
352 /// Connections that don't send complete HTTP headers within `header_timeout` are closed.
353 /// This prevents slowloris attacks and resource exhaustion from incomplete requests. The
354 /// timeout applies only to the header-read phase — once a complete header block has been
355 /// read, the connection is handed off with no further time bound, so long-lived response
356 /// bodies (e.g. the live-reload SSE stream from [`Server::with_live_reload`]) are not cut
357 /// off mid-stream.
358 ///
359 /// # Arguments
360 ///
361 /// * `header_timeout` - Maximum time to wait for complete HTTP headers on each connection.
362 ///
363 /// # Returns
364 ///
365 /// - `Ok((u16, ServerHandle))` with the ephemeral port number assigned by the OS and
366 /// a handle for graceful shutdown.
367 /// - `Err(StaticError::Io)` if binding to the socket fails.
368 ///
369 /// # Example
370 ///
371 /// ```no_run
372 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
373 /// use mini_static::Server;
374 /// use std::path::Path;
375 /// use std::time::Duration;
376 ///
377 /// let server = Server::new(Path::new("./public"))?;
378 /// let (port, handle) = server.run(Duration::from_secs(30)).await?;
379 /// println!("Server running on http://127.0.0.1:{}", port);
380 /// // ... later, to stop it gracefully:
381 /// handle.shutdown().await;
382 /// # Ok(())
383 /// # }
384 /// ```
385 pub async fn run(&self, header_timeout: Duration) -> Result<(u16, ServerHandle), StaticError> {
386 let addr: SocketAddr = ([127, 0, 0, 1], 0).into();
387 self.run_on(addr, header_timeout).await
388 }
389
390 /// Run the server on all interfaces (0.0.0.0) with a configurable header-read timeout.
391 ///
392 /// Binds to a specified port on all network interfaces. Useful for containerized
393 /// deployments, reverse-proxy setups, or services that need to accept connections
394 /// from anywhere. Spawns the server in a background Tokio task and returns immediately
395 /// with the assigned port and a [`ServerHandle`].
396 ///
397 /// # Header-Read Timeout
398 ///
399 /// Connections that don't send complete HTTP headers within `header_timeout` are closed.
400 /// This prevents slowloris attacks and resource exhaustion from incomplete requests. The
401 /// timeout applies only to the header-read phase — once a complete header block has been
402 /// read, the connection is handed off with no further time bound, so long-lived response
403 /// bodies (e.g. the live-reload SSE stream from [`Server::with_live_reload`]) are not cut
404 /// off mid-stream.
405 ///
406 /// # Arguments
407 ///
408 /// * `port` - Port number to bind to (0 for ephemeral port assignment).
409 /// * `header_timeout` - Maximum time to wait for complete HTTP headers on each connection.
410 ///
411 /// # Returns
412 ///
413 /// - `Ok((u16, ServerHandle))` with the assigned port number and a handle for graceful shutdown.
414 /// - `Err(StaticError::Io)` if binding to the socket fails.
415 ///
416 /// # Example
417 ///
418 /// ```no_run
419 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
420 /// use mini_static::Server;
421 /// use std::path::Path;
422 /// use std::time::Duration;
423 ///
424 /// let server = Server::new(Path::new("./public"))?;
425 /// let (_port, handle) = server.run_all(8080, Duration::from_secs(30)).await?;
426 /// println!("Server listening on 0.0.0.0:8080");
427 /// handle.shutdown().await;
428 /// # Ok(())
429 /// # }
430 /// ```
431 pub async fn run_all(&self, port: u16, header_timeout: Duration) -> Result<(u16, ServerHandle), StaticError> {
432 let addr: SocketAddr = ([0, 0, 0, 0], port).into();
433 self.run_on(addr, header_timeout).await
434 }
435
436 /// Run the server on loopback (127.0.0.1) with a default header-read timeout.
437 ///
438 /// Convenience wrapper around `run()` that uses a default 30-second header-read timeout.
439 /// Returns immediately with the ephemeral port number and a [`ServerHandle`]; the server
440 /// continues in a background Tokio task until the handle's `shutdown()` is awaited or
441 /// the Tokio runtime shuts down.
442 ///
443 /// This is the recommended method for tests and lightweight services that don't require
444 /// custom timeout configuration.
445 ///
446 /// # Returns
447 ///
448 /// - `Ok((u16, ServerHandle))` with the ephemeral port number assigned by the OS and
449 /// a handle for graceful shutdown.
450 /// - `Err(StaticError::Io)` if binding to the socket fails.
451 ///
452 /// # Example
453 ///
454 /// ```no_run
455 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
456 /// use mini_static::Server;
457 /// use std::path::Path;
458 ///
459 /// let server = Server::new(Path::new("./public"))?;
460 /// let (port, handle) = server.run_ephemeral().await?;
461 /// println!("Server ready on http://127.0.0.1:{}", port);
462 /// handle.shutdown().await;
463 /// # Ok(())
464 /// # }
465 /// ```
466 pub async fn run_ephemeral(&self) -> Result<(u16, ServerHandle), StaticError> {
467 self.run(Duration::from_secs(30)).await
468 }
469
470 /// Handle an HTTP request asynchronously, streaming file bodies to the client.
471 ///
472 /// This is the method to call when embedding `mini-static` inside another async
473 /// server's request-handling path (e.g. as a catch-all fallback route). Unlike
474 /// [`Server::handle_request`] and its synchronous siblings, this method never blocks
475 /// the calling task: path resolution runs on Tokio's blocking-thread pool via
476 /// `spawn_blocking`, and the file is read via async I/O.
477 ///
478 /// File responses are backed by `FileBody`, which reads and hands off one 64 KB
479 /// chunk to hyper at a time as `poll_frame` is driven — memory use stays bounded to
480 /// one chunk per in-flight response regardless of file size, and no chunk is copied
481 /// or zero-filled beyond what the read syscall itself writes.
482 ///
483 /// Conditional requests (If-None-Match, If-Modified-Since) are honored: if the
484 /// request includes a validator that matches the file's ETag, returns 304 Not Modified.
485 pub async fn handle_request_async(
486 &self,
487 method: &Method,
488 request_path: &str,
489 if_none_match: Option<&str>,
490 if_modified_since: Option<&str>,
491 ) -> Response<ResponseBody> {
492 // Gate on HTTP method
493 if method != Method::GET && method != Method::HEAD {
494 return finish(Response::builder()
495 .status(StatusCode::METHOD_NOT_ALLOWED)
496 .header("Allow", "GET, HEAD")
497 .header("X-Content-Type-Options", "nosniff")
498 .body(into_response_body(Full::new(Bytes::from("method not allowed\n")))));
499 }
500
501 // Live-reload SSE stream — only reachable when `with_live_reload()` was called
502 // and the server was started via `run()`/`run_all()`/`run_on()` (those are the
503 // only paths that populate `broadcaster`).
504 if *method == Method::GET && request_path == reload::LIVE_RELOAD_PATH {
505 if let Some(broadcaster) = &self.broadcaster {
506 let rx = broadcaster.subscribe();
507 return finish(Response::builder()
508 .status(StatusCode::OK)
509 .header("Content-Type", "text/event-stream")
510 .header("Cache-Control", "no-cache")
511 .header("Connection", "keep-alive")
512 .header("X-Content-Type-Options", "nosniff")
513 .body(ResponseBody::Sse(SseBody::new(rx))));
514 }
515 }
516
517 // `resolve()` does blocking filesystem syscalls (`canonicalize()`, up to two per
518 // request). Running those directly in this `async fn` would block whichever
519 // Tokio worker thread happens to be driving it, stalling every other task
520 // scheduled on that thread for the duration of the syscalls. `spawn_blocking`
521 // moves the work onto Tokio's dedicated blocking thread pool instead.
522 let server = self.clone();
523 let owned_request_path = request_path.to_string();
524 let resolved = tokio::task::spawn_blocking(move || server.resolve(&owned_request_path)).await;
525 let resolved = match resolved {
526 Ok(r) => r,
527 Err(_) => return internal_error_response(),
528 };
529
530 match resolved {
531 Ok(path) => {
532 let decoded_request_path = resolve::decode_request_path(request_path);
533 if path.file_name().is_some_and(|name| name == "index.html")
534 && !decoded_request_path.ends_with('/')
535 && !decoded_request_path.ends_with("index.html")
536 {
537 let location = format!("{}/", request_path.trim_end_matches('/'));
538 // `location` is built from the (attacker-controlled) request path;
539 // `finish()` degrades to 400 instead of panicking if it ever contains
540 // bytes invalid in a header value.
541 return finish(Response::builder()
542 .status(StatusCode::MOVED_PERMANENTLY)
543 .header("Location", location)
544 .header("X-Content-Type-Options", "nosniff")
545 .body(into_response_body(Full::new(Bytes::from("moved\n")))));
546 }
547
548 // Use async file operations for streaming
549 let file = match File::open(&path).await {
550 Ok(f) => f,
551 Err(_) => return internal_error_response(),
552 };
553
554 let metadata = match file.metadata().await {
555 Ok(m) => m,
556 Err(_) => return internal_error_response(),
557 };
558
559 let mut file_size = metadata.len();
560 let etag = generate_etag(&metadata);
561
562 // Check If-None-Match (ETag) for 304 Not Modified
563 if let Some(if_none_match) = if_none_match {
564 if is_etag_match(if_none_match, &etag) {
565 return finish(Response::builder()
566 .status(StatusCode::NOT_MODIFIED)
567 .header("ETag", etag)
568 .body(into_response_body(Full::new(Bytes::new()))));
569 }
570 }
571
572 // Check If-Modified-Since (mtime) for 304 Not Modified
573 if let Some(if_modified_since) = if_modified_since {
574 if is_not_modified_since(if_modified_since, &metadata) {
575 return finish(Response::builder()
576 .status(StatusCode::NOT_MODIFIED)
577 .header("ETag", etag)
578 .body(into_response_body(Full::new(Bytes::new()))));
579 }
580 }
581
582 let content_type = mime_type_for_path(&path);
583
584 // HEAD must not return a body (RFC 9110); skip opening the read stream
585 // entirely since we'd just discard every chunk.
586 //
587 // Live-reload HTML injection reads the whole file into memory instead of
588 // streaming it — acceptable only because it's gated on `broadcaster`
589 // being set, i.e. `with_live_reload()` was called for local development;
590 // the streamed path remains untouched for every production response.
591 let body: ResponseBody = if *method == Method::HEAD {
592 into_response_body(Full::new(Bytes::new()))
593 } else if self.broadcaster.is_some() && content_type.starts_with("text/html") {
594 let mut html = Vec::with_capacity(file_size as usize);
595 let mut file = file;
596 if file.read_to_end(&mut html).await.is_err() {
597 return internal_error_response();
598 }
599 reload::inject_reload_script(&mut html);
600 file_size = html.len() as u64;
601 into_response_body(Full::new(Bytes::from(html)))
602 } else {
603 ResponseBody::Streamed(FileBody::new(file))
604 };
605
606 finish(Response::builder()
607 .status(StatusCode::OK)
608 .header("X-Content-Type-Options", "nosniff")
609 .header("Content-Type", content_type)
610 .header("Content-Length", file_size.to_string())
611 .header("ETag", etag)
612 .body(body))
613 }
614 Err(e) => {
615 let message = e.user_message();
616 let body = format!("{}\n", message);
617
618 finish(Response::builder()
619 .status(StatusCode::NOT_FOUND)
620 .header("X-Content-Type-Options", "nosniff")
621 .body(into_response_body(Full::new(Bytes::from(body)))))
622 }
623 }
624 }
625
626 /// Handle an HTTP request with method and optional Range/If-Range headers (synchronous API).
627 ///
628 /// This is the synchronous version of request handling used internally by the
629 /// async server loop. For most use cases, prefer using `run()` or `run_ephemeral()`
630 /// which handle the full async lifecycle.
631 ///
632 /// Only GET and HEAD methods are allowed; other methods return 405 Method Not Allowed.
633 /// All errors (missing files, traversal attempts, I/O failures) are returned as 404
634 /// to avoid leaking filesystem structure information.
635 ///
636 /// # Range Request Handling
637 ///
638 /// mini-static does not yet serve `206 Partial Content` — every request,
639 /// ranged or not, gets the full body with `200`. This is RFC 9110-correct behavior
640 /// (as opposed to incorrectly answering `416`), but partial-content serving is
641 /// deferred to a later phase.
642 ///
643 /// # Arguments
644 ///
645 /// * `method` - The HTTP method (GET and HEAD only).
646 /// * `request_path` - The HTTP request path (e.g., `/index.html`).
647 /// * `_range_header` - Optional Range header (currently unused).
648 /// * `_if_range_header` - Optional If-Range header (currently unused).
649 pub fn handle_request_with_headers(
650 &self,
651 method: &Method,
652 request_path: &str,
653 _range_header: Option<&str>,
654 _if_range_header: Option<&str>,
655 ) -> Response<ResponseBody> {
656 // Gate on HTTP method
657 if method != Method::GET && method != Method::HEAD {
658 return finish(Response::builder()
659 .status(StatusCode::METHOD_NOT_ALLOWED)
660 .header("Allow", "GET, HEAD")
661 .header("X-Content-Type-Options", "nosniff")
662 .body(into_response_body(Full::new(Bytes::from("method not allowed\n")))));
663 }
664
665 // Method is allowed; resolve the path
666 match self.resolve(request_path) {
667 Ok(path) => {
668 // Check if resolved path is index.html but request_path doesn't end with /
669 // If so, redirect to path/ to establish correct base for relative links.
670 // Compare against the *decoded* request path so a percent-encoded explicit
671 // request for index.html (e.g. `/docs/index.htm%6c`) is recognized as such
672 // instead of producing a redirect to a still-encoded, broken Location.
673 let decoded_request_path = resolve::decode_request_path(request_path);
674 if path.file_name().is_some_and(|name| name == "index.html")
675 && !decoded_request_path.ends_with('/')
676 && !decoded_request_path.ends_with("index.html")
677 {
678 let location = format!("{}/", request_path.trim_end_matches('/'));
679
680 // Location is built from the (attacker-controlled) request path;
681 // `finish()` degrades to 400 instead of panicking if it ever contains
682 // bytes invalid in a header value.
683 return finish(Response::builder()
684 .status(StatusCode::MOVED_PERMANENTLY)
685 .header("Location", location)
686 .header("X-Content-Type-Options", "nosniff")
687 .body(into_response_body(Full::new(Bytes::from("moved\n")))));
688 }
689
690 let file = match fs::File::open(&path) {
691 Ok(f) => f,
692 Err(_) => return internal_error_response(),
693 };
694 let metadata = match file.metadata() {
695 Ok(m) => m,
696 Err(_) => return internal_error_response(),
697 };
698 let file_size = metadata.len();
699 let etag = generate_etag(&metadata);
700
701 // HEAD must not return a body (RFC 9110); avoid reading file content we'd
702 // just discard.
703 let body_bytes = if *method == Method::HEAD {
704 Bytes::new()
705 } else {
706 let mut buf = Vec::with_capacity(file_size as usize);
707 let mut file = file;
708 if file.read_to_end(&mut buf).is_err() {
709 return internal_error_response();
710 }
711 Bytes::from(buf)
712 };
713
714 finish(Response::builder()
715 .status(StatusCode::OK)
716 .header("X-Content-Type-Options", "nosniff")
717 .header("Content-Length", file_size.to_string())
718 .header("ETag", etag)
719 .body(into_response_body(Full::new(body_bytes))))
720 }
721 Err(e) => {
722 let message = e.user_message();
723 let body = format!("{}\n", message);
724
725 finish(Response::builder()
726 .status(StatusCode::NOT_FOUND)
727 .header("X-Content-Type-Options", "nosniff")
728 .body(into_response_body(Full::new(Bytes::from(body)))))
729 }
730 }
731 }
732}
733
734/// Ceiling on how many bytes `read_header_prefix` buffers before giving up. Without this,
735/// a client that trickles bytes forever without ever sending the terminating blank line
736/// could grow the buffer without limit — the header-read timeout alone doesn't bound
737/// memory, only wall-clock time, and a sufficiently patient sender could still send
738/// unbounded data before the deadline fires.
739const MAX_HEADER_BYTES: usize = 64 * 1024;
740
741/// Why `read_header_prefix` gave up before seeing a complete header block. Every variant
742/// is a legitimate reason to drop the connection — none is treated specially by the
743/// caller today, but the distinction is worth preserving for anyone debugging this later.
744#[derive(Debug)]
745enum HeaderReadError {
746 /// The client closed the connection (or shut down its write half) before sending a
747 /// complete header block.
748 ConnectionClosed,
749 /// More than `MAX_HEADER_BYTES` were buffered without seeing `\r\n\r\n`.
750 TooLarge,
751 /// The underlying socket read failed. No caller inspects the wrapped error today —
752 /// every outcome here just drops the connection — but it's kept rather than
753 /// discarded so a future `log` feature (see the crate's planned Cargo feature of the
754 /// same name) has the real I/O error to report instead of an opaque unit variant.
755 #[allow(dead_code)]
756 Io(std::io::Error),
757}
758
759/// Reads from `stream` until a complete HTTP header block (`\r\n\r\n`) has been buffered,
760/// returning every byte read so far — which may include bytes past the header block
761/// (request body, or a second pipelined request) if the client sent them in the same
762/// read. Callers pair this with `tokio::time::timeout` to bound how long the header phase
763/// itself may take; this function has no timeout of its own, only the size ceiling in
764/// `MAX_HEADER_BYTES`.
765async fn read_header_prefix(stream: &mut TcpStream) -> Result<Vec<u8>, HeaderReadError> {
766 let mut buf = Vec::new();
767 let mut chunk = [0u8; 4096];
768
769 loop {
770 let n = stream.read(&mut chunk).await.map_err(HeaderReadError::Io)?;
771 if n == 0 {
772 return Err(HeaderReadError::ConnectionClosed);
773 }
774 buf.extend_from_slice(&chunk[..n]);
775
776 if buf.len() > MAX_HEADER_BYTES {
777 return Err(HeaderReadError::TooLarge);
778 }
779 if buf.windows(4).any(|w| w == b"\r\n\r\n") {
780 return Ok(buf);
781 }
782 }
783}
784
785/// Wraps an accepted `TcpStream` whose header block has already been drained into
786/// `prefix` (by `read_header_prefix`, ahead of the connection being handed to hyper).
787/// Reads replay `prefix` first, then fall through to the live socket — so hyper sees
788/// exactly the byte stream it would have seen without the pre-read, just sourced from two
789/// buffers back-to-back instead of one continuous one. Writes pass straight through.
790struct PrefixedIo {
791 prefix: Bytes,
792 prefix_pos: usize,
793 inner: TcpStream,
794}
795
796impl PrefixedIo {
797 fn new(prefix: Vec<u8>, inner: TcpStream) -> Self {
798 PrefixedIo {
799 prefix: Bytes::from(prefix),
800 prefix_pos: 0,
801 inner,
802 }
803 }
804}
805
806impl AsyncRead for PrefixedIo {
807 fn poll_read(
808 self: Pin<&mut Self>,
809 cx: &mut Context<'_>,
810 buf: &mut ReadBuf<'_>,
811 ) -> Poll<std::io::Result<()>> {
812 let this = self.get_mut();
813 if this.prefix_pos < this.prefix.len() {
814 let remaining = &this.prefix[this.prefix_pos..];
815 let n = remaining.len().min(buf.remaining());
816 buf.put_slice(&remaining[..n]);
817 this.prefix_pos += n;
818 return Poll::Ready(Ok(()));
819 }
820 Pin::new(&mut this.inner).poll_read(cx, buf)
821 }
822}
823
824impl AsyncWrite for PrefixedIo {
825 fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll<std::io::Result<usize>> {
826 Pin::new(&mut self.get_mut().inner).poll_write(cx, buf)
827 }
828
829 fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
830 Pin::new(&mut self.get_mut().inner).poll_flush(cx)
831 }
832
833 fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
834 Pin::new(&mut self.get_mut().inner).poll_shutdown(cx)
835 }
836}
837
838/// Wires an accepted connection up to the hyper HTTP/1 service.
839///
840/// `header_timeout` bounds only the header-read phase (`read_header_prefix`, run before
841/// hyper ever sees the connection). Once a complete header block has been read, the
842/// connection is handed to hyper with no further time bound — deliberately, since a
843/// response body may legitimately outlive `header_timeout` by design (the live-reload SSE
844/// stream is the motivating case: it stays open until a watched file changes, which may
845/// be minutes or hours after the request). Wrapping the whole connection lifetime in
846/// `header_timeout` — the prior implementation — silently truncated exactly that stream
847/// once `header_timeout` elapsed, aborting the response mid-write after headers had
848/// already been sent (the client observes this as a chunked-encoding error, not a clean
849/// close). The connection-count ceiling (`Server::with_max_connections`) is what bounds
850/// resource use from connections held open indefinitely, not this timeout.
851async fn serve_connection(mut stream: TcpStream, server: Server, header_timeout: Duration) {
852 let prefix = match timeout(header_timeout, read_header_prefix(&mut stream)).await {
853 Ok(Ok(prefix)) => prefix,
854 Ok(Err(_)) | Err(_) => return,
855 };
856
857 let io = TokioIo::new(PrefixedIo::new(prefix, stream));
858 let svc = service_fn(move |req: Request<Incoming>| {
859 let server = server.clone();
860 async move {
861 let method = req.method().clone();
862 let path = req.uri().path().to_string();
863 let if_none_match = req.headers().get("if-none-match").and_then(|v| v.to_str().ok());
864 let if_modified_since = req.headers().get("if-modified-since").and_then(|v| v.to_str().ok());
865 let resp = server.handle_request_async(&method, &path, if_none_match, if_modified_since).await;
866 Ok::<_, Infallible>(resp)
867 }
868 });
869 let _ = AutoBuilder::new(TokioExecutor::new()).serve_connection(io, svc).await;
870}
871
872/// A handle to a server started by `Server::run()` or `Server::run_ephemeral()`.
873///
874/// Dropping this handle without calling `shutdown()` leaves the server running in the
875/// background for the life of the process — the same behavior `run()` always had before
876/// this handle existed. Call `shutdown()` to stop accepting new connections and wait for
877/// already-accepted connections to finish before returning.
878pub struct ServerHandle {
879 shutdown_tx: Option<tokio::sync::oneshot::Sender<()>>,
880 accept_task: tokio::task::JoinHandle<()>,
881}
882
883/// Default grace period `ServerHandle::shutdown()` waits for in-flight connections to
884/// finish on their own before aborting whatever is left. A connection with no
885/// self-imposed end — an open live-reload SSE stream, or any keep-alive connection whose
886/// peer simply never closes it — would otherwise let `shutdown()` hang forever waiting
887/// for it to finish naturally. Every wait in this crate has a stated upper bound;
888/// shutdown is no exception.
889const DEFAULT_SHUTDOWN_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
890
891impl ServerHandle {
892 /// Stop accepting new connections and wait up to `DEFAULT_SHUTDOWN_DRAIN_TIMEOUT`
893 /// (5s) for in-flight connections to finish on their own. Equivalent to
894 /// `shutdown_with_timeout(DEFAULT_SHUTDOWN_DRAIN_TIMEOUT)` — see that method for what
895 /// happens to connections still open once the grace period elapses.
896 pub async fn shutdown(self) {
897 self.shutdown_with_timeout(DEFAULT_SHUTDOWN_DRAIN_TIMEOUT).await;
898 }
899
900 /// Stop accepting new connections and wait up to `drain_timeout` for in-flight
901 /// connections to finish on their own.
902 ///
903 /// Connections still open once `drain_timeout` elapses are aborted rather than
904 /// waited on further: dropping the accept task drops its `JoinSet`, which aborts
905 /// every task still tracked in it (see `tokio::task::JoinSet`'s own drop behavior) —
906 /// which in turn drops each connection's socket, closing it. This is what bounds
907 /// shutdown when a connection has no natural end of its own (the live-reload SSE
908 /// stream is the motivating case: it stays open until a watched file changes, which
909 /// may never happen before the process needs to exit).
910 pub async fn shutdown_with_timeout(mut self, drain_timeout: Duration) {
911 if let Some(tx) = self.shutdown_tx.take() {
912 let _ = tx.send(());
913 }
914 if timeout(drain_timeout, &mut self.accept_task).await.is_err() {
915 self.accept_task.abort();
916 }
917 }
918}
919
920fn into_response_body(body: Full<Bytes>) -> ResponseBody {
921 ResponseBody::Buffered(body)
922}
923
924/// Finishes building a response, degrading to a generic 400 instead of panicking if any
925/// header value turns out to be invalid for use as an HTTP header value.
926///
927/// Every header value that reaches `Response::builder()` in this module is either a
928/// static string or formatted from internal, already-validated data (a byte count, an
929/// mtime, a fixed method list) — none of it can actually fail today. But `.unwrap()`
930/// on that assumption is exactly the kind of thing that turns "can't happen" into a
931/// production panic the day someone adds a header built from new input without
932/// re-deriving that guarantee. Routing every response through this one fallible path
933/// means that mistake fails safe instead of panicking.
934fn finish(built: Result<Response<ResponseBody>, hyper::http::Error>) -> Response<ResponseBody> {
935 built.unwrap_or_else(|_| bad_request_response())
936}
937
938/// Default maximum concurrent connections, overridable via `Server::with_max_connections`.
939const DEFAULT_MAX_CONNECTIONS: usize = 1024;
940
941// `internal_error_response()` and `bad_request_response()` are the fallback responses
942// `finish()` itself degrades to — every header and body here is a fixed string with no
943// external input, so `.body(...)` cannot fail. They can't be routed through `finish()`
944// without it degrading to itself on failure.
945fn internal_error_response() -> Response<ResponseBody> {
946 Response::builder()
947 .status(StatusCode::INTERNAL_SERVER_ERROR)
948 .header("X-Content-Type-Options", "nosniff")
949 .body(into_response_body(Full::new(Bytes::from(
950 "internal server error\n",
951 ))))
952 .unwrap()
953}
954
955fn bad_request_response() -> Response<ResponseBody> {
956 Response::builder()
957 .status(StatusCode::BAD_REQUEST)
958 .header("X-Content-Type-Options", "nosniff")
959 .body(into_response_body(Full::new(Bytes::from("bad request\n"))))
960 .unwrap()
961}
962
963/// Generate an ETag for a file based on modification time and size.
964///
965/// Format: `"<size>-<mtime_secs>"`
966fn generate_etag(metadata: &fs::Metadata) -> String {
967 let size = metadata.len();
968 let mtime = metadata
969 .modified()
970 .ok()
971 .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
972 .map(|d| d.as_secs())
973 .unwrap_or(0);
974 format!("\"{}-{}\"", size, mtime)
975}
976
977/// Determine MIME type from file path extension.
978fn mime_type_for_path(path: &Path) -> &'static str {
979 path.extension()
980 .and_then(|ext| ext.to_str())
981 .and_then(|ext| match ext.to_lowercase().as_str() {
982 "html" | "htm" => Some("text/html; charset=utf-8"),
983 "css" => Some("text/css; charset=utf-8"),
984 "js" => Some("application/javascript; charset=utf-8"),
985 "json" => Some("application/json; charset=utf-8"),
986 "svg" => Some("image/svg+xml"),
987 "png" => Some("image/png"),
988 "jpg" | "jpeg" => Some("image/jpeg"),
989 "gif" => Some("image/gif"),
990 "webp" => Some("image/webp"),
991 "ico" => Some("image/x-icon"),
992 "woff" => Some("font/woff"),
993 "woff2" => Some("font/woff2"),
994 "ttf" => Some("font/ttf"),
995 "md" | "markdown" => Some("text/markdown; charset=utf-8"),
996 "txt" => Some("text/plain; charset=utf-8"),
997 "xml" => Some("application/xml"),
998 "pdf" => Some("application/pdf"),
999 "zip" => Some("application/zip"),
1000 _ => None,
1001 })
1002 .unwrap_or("application/octet-stream")
1003}
1004
1005/// Check if the If-None-Match header matches the current ETag.
1006/// Handles both exact match and wildcard (*) comparison per RFC 9110.
1007fn is_etag_match(if_none_match: &str, etag: &str) -> bool {
1008 if if_none_match == "*" {
1009 return true;
1010 }
1011 if_none_match.split(',').any(|tag| tag.trim() == etag)
1012}
1013
1014/// Check if If-Modified-Since indicates the file hasn't been modified.
1015/// Returns true if the file's mtime is before/equal to the If-Modified-Since timestamp.
1016fn is_not_modified_since(if_modified_since: &str, metadata: &fs::Metadata) -> bool {
1017 let file_mtime = metadata
1018 .modified()
1019 .ok()
1020 .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
1021 .map(|d| d.as_secs())
1022 .unwrap_or(0);
1023
1024 // Parse the If-Modified-Since header as an HTTP-date (RFC 9110 Section 5.6.7).
1025 // For simplicity, try to parse as a simple Unix timestamp first, then fall back to
1026 // a basic string comparison. A production implementation would use a proper
1027 // RFC 2822 / RFC 9110 date parser, but for testing we can be lenient.
1028 if let Ok(client_time) = if_modified_since.parse::<u64>() {
1029 return file_mtime <= client_time;
1030 }
1031
1032 // Fallback: if parsing fails, be conservative and don't return 304.
1033 false
1034}
1035
1036#[cfg(test)]
1037mod file_body_tests {
1038 use super::*;
1039 use crate::handler::FILE_CHUNK_SIZE;
1040 use http_body_util::BodyExt;
1041
1042 // Disproves the prior implementation, which read every chunk into a `Vec` and
1043 // only wrapped the whole result in a single `Full` frame at the end — that
1044 // implementation would fail this test with `frame_count == 1` and
1045 // `max_frame_len == file size`, regardless of `FILE_CHUNK_SIZE`.
1046 #[tokio::test]
1047 async fn file_body_yields_multiple_bounded_chunks_not_one_buffered_frame() {
1048 let dir = tempfile::TempDir::new().unwrap();
1049 let path = dir.path().join("big.bin");
1050 let content = vec![7u8; FILE_CHUNK_SIZE * 3 + 12_345];
1051 fs::write(&path, &content).unwrap();
1052
1053 let file = File::open(&path).await.unwrap();
1054 let mut body = FileBody::new(file);
1055
1056 let mut frame_count = 0usize;
1057 let mut max_frame_len = 0usize;
1058 let mut reassembled = Vec::new();
1059
1060 while let Some(frame) = body.frame().await {
1061 let frame = frame.unwrap();
1062 let data = frame.into_data().unwrap();
1063 frame_count += 1;
1064 max_frame_len = max_frame_len.max(data.len());
1065 reassembled.extend_from_slice(&data);
1066 }
1067
1068 assert!(
1069 frame_count > 1,
1070 "expected the file to be delivered as multiple frames, got {frame_count}"
1071 );
1072 assert!(
1073 max_frame_len <= FILE_CHUNK_SIZE,
1074 "no single frame should exceed the chunk size ({FILE_CHUNK_SIZE}), got {max_frame_len}"
1075 );
1076 assert_eq!(reassembled, content, "reassembled chunks must match original file content exactly");
1077 }
1078}
1079
1080#[cfg(test)]
1081mod accept_tests {
1082 use super::*;
1083 use std::sync::atomic::{AtomicUsize, Ordering};
1084 use std::sync::Mutex;
1085
1086 #[test]
1087 fn backoff_doubles_up_to_max() {
1088 let mut backoff = Backoff::new();
1089 let mut last = backoff.next_delay();
1090 assert_eq!(last, ACCEPT_BACKOFF_INITIAL);
1091
1092 // Double repeatedly; it must stop growing once it hits the cap rather than
1093 // continuing to double forever (a fixed upper bound, not an unbounded retry).
1094 for _ in 0..20 {
1095 last = backoff.next_delay();
1096 }
1097 assert_eq!(last, ACCEPT_BACKOFF_MAX);
1098 }
1099
1100 #[test]
1101 fn backoff_reset_returns_to_initial_delay() {
1102 let mut backoff = Backoff::new();
1103 backoff.next_delay();
1104 backoff.next_delay();
1105 backoff.reset();
1106 assert_eq!(backoff.next_delay(), ACCEPT_BACKOFF_INITIAL);
1107 }
1108
1109 /// Fails `accept()` a fixed number of times, recording the (paused, virtual)
1110 /// instant of each attempt, before delegating to a real listener so the caller can
1111 /// eventually succeed.
1112 struct FlakyListener {
1113 inner: TcpListener,
1114 remaining_failures: AtomicUsize,
1115 attempts: Mutex<Vec<tokio::time::Instant>>,
1116 }
1117
1118 impl TcpAccept for FlakyListener {
1119 async fn accept(&self) -> std::io::Result<(TcpStream, SocketAddr)> {
1120 self.attempts.lock().unwrap().push(tokio::time::Instant::now());
1121 if self.remaining_failures.fetch_sub(1, Ordering::SeqCst) > 0 {
1122 Err(std::io::Error::other("simulated accept error"))
1123 } else {
1124 TcpAccept::accept(&self.inner).await
1125 }
1126 }
1127 }
1128
1129 // Disproves the prior implementation, which broke out of the accept loop entirely
1130 // on the first `accept()` error — permanently ending the server. This test would
1131 // also fail against a naive `continue`-only fix (no backoff): the recorded gaps
1132 // between attempts would collapse to ~0 (a busy spin) instead of the expected
1133 // exponentially growing delays.
1134 #[tokio::test(start_paused = true)]
1135 async fn accept_loop_backs_off_between_repeated_errors_instead_of_busy_spinning() {
1136 let inner = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
1137 let addr = inner.local_addr().unwrap();
1138
1139 let flaky = FlakyListener {
1140 inner,
1141 remaining_failures: AtomicUsize::new(5),
1142 attempts: Mutex::new(Vec::new()),
1143 };
1144
1145 tokio::spawn(async move {
1146 let _ = TcpStream::connect(addr).await;
1147 });
1148
1149 let semaphore = Arc::new(Semaphore::new(1));
1150 let mut backoff = Backoff::new();
1151 let result = accept_and_permit(&flaky, &mut backoff, &semaphore).await;
1152 assert!(result.is_some(), "accept should eventually succeed once the flaky listener stops failing");
1153
1154 let recorded = flaky.attempts.lock().unwrap();
1155 assert_eq!(recorded.len(), 6, "5 failures then 1 success");
1156
1157 let expected_gaps = [
1158 ACCEPT_BACKOFF_INITIAL,
1159 ACCEPT_BACKOFF_INITIAL * 2,
1160 ACCEPT_BACKOFF_INITIAL * 4,
1161 ACCEPT_BACKOFF_INITIAL * 8,
1162 ACCEPT_BACKOFF_INITIAL * 16,
1163 ];
1164 for (i, expected) in expected_gaps.iter().enumerate() {
1165 let gap = recorded[i + 1] - recorded[i];
1166 assert_eq!(
1167 gap, *expected,
1168 "gap between attempt {i} and {} should reflect the backoff delay, not a busy spin",
1169 i + 1
1170 );
1171 }
1172 }
1173}
1174
1175#[cfg(test)]
1176mod finish_tests {
1177 use super::*;
1178
1179 // Disproves a bare `.unwrap()` on the same builder: CR/LF is not a legal header
1180 // value byte (it would enable header/response splitting), so this construction is
1181 // guaranteed to make `.body(...)` return `Err`. Every real call site in this module
1182 // only ever builds header values from static strings or internally-formatted
1183 // numbers, so this test can't happen through normal use — it exists to prove
1184 // `finish()`'s fallback path actually works, not to exercise a reachable case.
1185 #[test]
1186 fn finish_degrades_to_400_on_invalid_header_value_instead_of_panicking() {
1187 let built = Response::builder()
1188 .status(StatusCode::OK)
1189 .header("X-Test", "invalid\r\nvalue")
1190 .body(into_response_body(Full::new(Bytes::new())));
1191 assert!(built.is_err(), "CR/LF in a header value should be rejected by the builder");
1192
1193 let response = finish(built);
1194 assert_eq!(
1195 response.status(),
1196 StatusCode::BAD_REQUEST,
1197 "finish() should degrade to 400 rather than panicking on an invalid header value"
1198 );
1199 }
1200}
1201
1202#[cfg(test)]
1203mod header_prefix_tests {
1204 use super::*;
1205 use tokio::io::AsyncWriteExt;
1206
1207 /// Binds an ephemeral listener, connects a client to it, and returns both ends —
1208 /// `(server_side, client_side)` — so a test can drive `read_header_prefix` against a
1209 /// real socket without a full `Server`/`serve_connection` in the loop.
1210 async fn connected_pair() -> (TcpStream, TcpStream) {
1211 let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
1212 let addr = listener.local_addr().unwrap();
1213 let client = TcpStream::connect(addr).await.unwrap();
1214 let (server_side, _) = listener.accept().await.unwrap();
1215 (server_side, client)
1216 }
1217
1218 #[tokio::test]
1219 async fn reads_exactly_up_to_and_including_the_terminating_blank_line() {
1220 let (mut server_side, mut client) = connected_pair().await;
1221
1222 client
1223 .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n")
1224 .await
1225 .unwrap();
1226
1227 let prefix = read_header_prefix(&mut server_side).await.unwrap_or_else(|_| {
1228 panic!("expected a complete header block to be read");
1229 });
1230
1231 assert_eq!(prefix, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
1232 }
1233
1234 // Disproves a version that only inspects the newest chunk for `\r\n\r\n`: writing the
1235 // blank line in a separate write (and thus, almost always, a separate read) after the
1236 // rest of the headers would make that version wait forever, since the terminator
1237 // never appears within a single chunk.
1238 #[tokio::test]
1239 async fn assembles_a_header_block_split_across_multiple_writes() {
1240 let (mut server_side, mut client) = connected_pair().await;
1241
1242 client.write_all(b"GET /page HTTP/1.1\r\n").await.unwrap();
1243 client.write_all(b"Host: localhost\r\n").await.unwrap();
1244 client.write_all(b"\r\n").await.unwrap();
1245
1246 let prefix = read_header_prefix(&mut server_side).await.unwrap_or_else(|_| {
1247 panic!("expected a complete header block to be read across multiple writes");
1248 });
1249
1250 assert_eq!(prefix, b"GET /page HTTP/1.1\r\nHost: localhost\r\n\r\n");
1251 }
1252
1253 // Bytes past the header block (a pipelined second request, here) must be preserved
1254 // verbatim in the returned prefix — `PrefixedIo` depends on this to replay them to
1255 // hyper untouched.
1256 #[tokio::test]
1257 async fn preserves_bytes_sent_past_the_header_block() {
1258 let (mut server_side, mut client) = connected_pair().await;
1259
1260 let first = b"GET /a HTTP/1.1\r\nHost: localhost\r\n\r\n";
1261 let second = b"GET /b HTTP/1.1\r\nHost: localhost\r\n\r\n";
1262 let mut sent = Vec::new();
1263 sent.extend_from_slice(first);
1264 sent.extend_from_slice(second);
1265 client.write_all(&sent).await.unwrap();
1266
1267 let prefix = read_header_prefix(&mut server_side).await.unwrap_or_else(|_| {
1268 panic!("expected a complete header block to be read");
1269 });
1270
1271 assert_eq!(&prefix, &sent, "pipelined bytes past the first header block must survive intact");
1272 }
1273
1274 #[tokio::test]
1275 async fn errors_with_connection_closed_when_client_disconnects_before_headers_complete() {
1276 let (mut server_side, client) = connected_pair().await;
1277 drop(client);
1278
1279 match read_header_prefix(&mut server_side).await {
1280 Err(HeaderReadError::ConnectionClosed) => {}
1281 Err(_) => panic!("expected ConnectionClosed, got a different error variant"),
1282 Ok(_) => panic!("expected an error, got a complete header block from a closed connection"),
1283 }
1284 }
1285
1286 // Disproves an unbounded buffer: without the `MAX_HEADER_BYTES` check, this would
1287 // hang consuming memory forever instead of erroring, since the client never sends the
1288 // terminating blank line.
1289 #[tokio::test]
1290 async fn errors_with_too_large_once_max_header_bytes_is_exceeded_without_a_terminator() {
1291 let (mut server_side, mut client) = connected_pair().await;
1292
1293 let garbage = vec![b'a'; MAX_HEADER_BYTES + 1];
1294 client.write_all(&garbage).await.unwrap();
1295
1296 match read_header_prefix(&mut server_side).await {
1297 Err(HeaderReadError::TooLarge) => {}
1298 Err(_) => panic!("expected TooLarge, got a different error variant"),
1299 Ok(_) => panic!("expected an error, got a complete header block from unterminated garbage"),
1300 }
1301 }
1302
1303 #[tokio::test]
1304 async fn prefixed_io_replays_the_prefix_before_reading_from_the_live_socket() {
1305 let (server_side, mut client) = connected_pair().await;
1306 let mut io = PrefixedIo::new(b"buffered-prefix".to_vec(), server_side);
1307
1308 client.write_all(b"-live-bytes").await.unwrap();
1309
1310 let mut collected = Vec::new();
1311 let mut chunk = [0u8; 8];
1312 while collected.len() < b"buffered-prefix-live-bytes".len() {
1313 let n = io.read(&mut chunk).await.unwrap();
1314 assert!(n > 0, "read returned 0 before all expected bytes arrived");
1315 collected.extend_from_slice(&chunk[..n]);
1316 }
1317
1318 assert_eq!(collected, b"buffered-prefix-live-bytes");
1319 }
1320}