Skip to main content

http_handle/
server.rs

1// SPDX-License-Identifier: Apache-2.0 OR MIT
2// Copyright (c) 2023 - 2026 HTTP Handle
3
4// src/server.rs
5
6//! Core HTTP server runtime.
7//!
8//! Use this module when you need a static-first HTTP server with predictable request parsing,
9//! policy-aware response generation, and portable runtime behavior across macOS, Linux, and WSL.
10//!
11//! The primary entrypoints are [`Server`] and [`ServerBuilder`].
12//!
13
14use crate::error::ServerError;
15use crate::request::Request;
16use crate::response::Response;
17use crossbeam_channel::{Receiver, Sender, unbounded};
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20use std::fs;
21use std::io;
22use std::net::{IpAddr, TcpListener, TcpStream};
23use std::path::{Path, PathBuf};
24use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
25#[cfg(test)]
26use std::sync::mpsc;
27use std::sync::{Arc, Mutex, Once, OnceLock, RwLock};
28use std::thread;
29use std::time::{Duration, Instant, UNIX_EPOCH};
30
31static SHUTDOWN_SIGNAL_SLOT: OnceLock<
32    Mutex<Option<Arc<ShutdownSignal>>>,
33> = OnceLock::new();
34static SIGNAL_HANDLER_INSTALL: Once = Once::new();
35/// Number of shards in the rate-limit table. Picked to be a power of two so
36/// the modulo collapses to a bitmask, and large enough that contention from
37/// concurrent requests across distinct IPs almost always lands on different
38/// shards.
39const RATE_LIMIT_SHARDS: usize = 16;
40type RateLimitShard = Mutex<HashMap<IpAddr, Vec<Instant>>>;
41type RateLimitTable = [RateLimitShard; RATE_LIMIT_SHARDS];
42static RATE_LIMIT_STATE: OnceLock<RateLimitTable> = OnceLock::new();
43
44/// Maximum entries kept in the ETag cache. The key is `(len, mtime_secs)`
45/// — collisions across distinct files with identical metadata are
46/// harmless because the etag string itself is identical by construction.
47const ETAG_CACHE_MAX: usize = 256;
48type EtagCache = RwLock<HashMap<(u64, u64), Arc<str>>>;
49static ETAG_CACHE: OnceLock<EtagCache> = OnceLock::new();
50static METRIC_REQUESTS_TOTAL: AtomicUsize = AtomicUsize::new(0);
51static METRIC_RESPONSES_4XX: AtomicUsize = AtomicUsize::new(0);
52static METRIC_RESPONSES_5XX: AtomicUsize = AtomicUsize::new(0);
53static METRIC_RATE_LIMITED: AtomicUsize = AtomicUsize::new(0);
54
55/// Serves static HTTP content with configurable runtime policies.
56///
57/// You use `Server` as the main entrypoint to bind an address, map requests to files
58/// under a document root, and apply response policies such as CORS, cache hints, and
59/// simple rate limiting.
60///
61/// For most production setups, prefer [`Server::builder`] so optional settings are
62/// explicit and readable.
63///
64/// # Examples
65///
66/// ```rust
67/// use http_handle::Server;
68///
69/// let server = Server::new("127.0.0.1:8080", ".");
70/// assert_eq!(server.address(), "127.0.0.1:8080");
71/// ```
72///
73/// # Panics
74///
75/// This type does not panic on construction.
76#[doc(alias = "http server")]
77#[doc(alias = "static file server")]
78#[derive(
79    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize,
80)]
81pub struct Server {
82    address: String,
83    document_root: PathBuf,
84    /// Canonicalized `document_root` cached at build time. Skipped from
85    /// serde so the wire shape of `Server` is unchanged; recomputed on
86    /// deserialize via `Default`.
87    #[serde(skip, default)]
88    canonical_document_root: PathBuf,
89    cors_enabled: Option<bool>,
90    cors_origins: Option<Vec<String>>,
91    custom_headers: Option<HashMap<String, String>>,
92    request_timeout: Option<Duration>,
93    connection_timeout: Option<Duration>,
94    rate_limit_per_minute: Option<usize>,
95    static_cache_ttl_secs: Option<u64>,
96    /// Maximum file size, in bytes, that the in-memory file-serve path
97    /// will fully buffer. Files exceeding this cap return 503; this
98    /// guards against the OOM vector where a 1 GB file load drives RSS
99    /// to N × file_size on N concurrent requests. None falls back to
100    /// `DEFAULT_MAX_BUFFERED_BODY_BYTES` (64 MiB).
101    max_buffered_body_bytes: Option<u64>,
102}
103
104/// Builds a [`Server`] with optional policy and timeout configuration.
105///
106/// You use `ServerBuilder` when you want a fluent, explicit configuration surface for
107/// CORS, custom headers, timeouts, and rate limiting.
108///
109/// # Examples
110///
111/// ```rust
112/// use http_handle::Server;
113///
114/// let server = Server::builder()
115///     .address("127.0.0.1:8080")
116///     .document_root(".")
117///     .enable_cors()
118///     .build()
119///     .expect("valid builder config");
120///
121/// assert_eq!(server.address(), "127.0.0.1:8080");
122/// ```
123///
124/// # Errors
125///
126/// Builder finalization fails when required fields are missing.
127///
128/// # Panics
129///
130/// This type does not panic under normal usage.
131#[doc(alias = "builder")]
132#[doc(alias = "configuration")]
133#[derive(Clone, Debug, Default)]
134pub struct ServerBuilder {
135    address: Option<String>,
136    document_root: Option<PathBuf>,
137    cors_enabled: Option<bool>,
138    cors_origins: Option<Vec<String>>,
139    custom_headers: Option<HashMap<String, String>>,
140    request_timeout: Option<Duration>,
141    connection_timeout: Option<Duration>,
142    rate_limit_per_minute: Option<usize>,
143    static_cache_ttl_secs: Option<u64>,
144    max_buffered_body_bytes: Option<u64>,
145}
146
147impl ServerBuilder {
148    /// Creates a new builder with no required fields set.
149    ///
150    /// # Examples
151    ///
152    /// ```rust
153    /// use http_handle::server::ServerBuilder;
154    ///
155    /// let builder = ServerBuilder::new();
156    /// let _ = builder;
157    /// assert_eq!(2 + 2, 4);
158    /// ```
159    ///
160    /// # Panics
161    ///
162    /// This function does not panic.
163    #[doc(alias = "new builder")]
164    pub fn new() -> Self {
165        Self::default()
166    }
167
168    /// Sets the bind address (`ip:port`) for the server.
169    ///
170    /// # Examples
171    ///
172    /// ```rust
173    /// use http_handle::Server;
174    ///
175    /// let server = Server::builder()
176    ///     .address("127.0.0.1:8080")
177    ///     .document_root(".")
178    ///     .build()
179    ///     .expect("builder should succeed");
180    /// assert_eq!(server.address(), "127.0.0.1:8080");
181    /// ```
182    ///
183    /// # Panics
184    ///
185    /// This function does not panic.
186    #[doc(alias = "bind address")]
187    pub fn address(mut self, address: &str) -> Self {
188        self.address = Some(address.to_string());
189        self
190    }
191
192    /// Sets the document root directory used for file resolution.
193    ///
194    /// # Examples
195    ///
196    /// ```rust
197    /// use http_handle::Server;
198    ///
199    /// let server = Server::builder()
200    ///     .address("127.0.0.1:8080")
201    ///     .document_root(".")
202    ///     .build()
203    ///     .expect("builder should succeed");
204    /// assert_eq!(server.document_root().as_path(), std::path::Path::new("."));
205    /// ```
206    ///
207    /// # Panics
208    ///
209    /// This function does not panic.
210    #[doc(alias = "document root")]
211    pub fn document_root(mut self, path: &str) -> Self {
212        self.document_root = Some(PathBuf::from(path));
213        self
214    }
215
216    /// Enables CORS with default settings
217    pub fn enable_cors(mut self) -> Self {
218        self.cors_enabled = Some(true);
219        self
220    }
221
222    /// Disables CORS
223    pub fn disable_cors(mut self) -> Self {
224        self.cors_enabled = Some(false);
225        self
226    }
227
228    /// Sets allowed CORS origins
229    pub fn cors_origins(mut self, origins: Vec<String>) -> Self {
230        self.cors_origins = Some(origins);
231        self.cors_enabled = Some(true); // Auto-enable CORS when origins are set
232        self
233    }
234
235    /// Adds a custom header that will be included in all responses
236    pub fn custom_header(mut self, name: &str, value: &str) -> Self {
237        let mut headers = self.custom_headers.unwrap_or_default();
238        let _ = headers.insert(name.to_string(), value.to_string());
239        self.custom_headers = Some(headers);
240        self
241    }
242
243    /// Sets multiple custom headers
244    pub fn custom_headers(
245        mut self,
246        headers: HashMap<String, String>,
247    ) -> Self {
248        self.custom_headers = Some(headers);
249        self
250    }
251
252    /// Sets the request timeout duration
253    pub fn request_timeout(mut self, timeout: Duration) -> Self {
254        self.request_timeout = Some(timeout);
255        self
256    }
257
258    /// Sets the connection timeout duration
259    pub fn connection_timeout(mut self, timeout: Duration) -> Self {
260        self.connection_timeout = Some(timeout);
261        self
262    }
263
264    /// Sets a simple per-IP request rate limit per minute.
265    pub fn rate_limit_per_minute(mut self, requests: usize) -> Self {
266        self.rate_limit_per_minute = Some(requests.max(1));
267        self
268    }
269
270    /// Sets a default static cache max-age (in seconds).
271    pub fn static_cache_ttl_secs(mut self, ttl: u64) -> Self {
272        self.static_cache_ttl_secs = Some(ttl);
273        self
274    }
275
276    /// Sets the maximum file size (in bytes) the in-memory
277    /// file-serve path will buffer before returning 503.
278    ///
279    /// Defaults to [`DEFAULT_MAX_BUFFERED_BODY_BYTES`] (64 MiB) when
280    /// unset. Lower this on memory-constrained hosts; raise it for
281    /// larger asset bundles where the operator has measured RSS
282    /// headroom under expected concurrent load.
283    ///
284    /// # Examples
285    ///
286    /// ```rust
287    /// use http_handle::Server;
288    ///
289    /// let server = Server::builder()
290    ///     .address("127.0.0.1:0")
291    ///     .document_root(".")
292    ///     .max_buffered_body_bytes(8 * 1024 * 1024) // 8 MiB cap
293    ///     .build()
294    ///     .expect("server");
295    /// assert_eq!(server.max_buffered_body_bytes(), 8 * 1024 * 1024);
296    /// ```
297    pub fn max_buffered_body_bytes(mut self, bytes: u64) -> Self {
298        self.max_buffered_body_bytes = Some(bytes);
299        self
300    }
301
302    /// Finalizes builder state into a [`Server`].
303    ///
304    /// # Examples
305    ///
306    /// ```rust
307    /// use http_handle::Server;
308    ///
309    /// let ok = Server::builder()
310    ///     .address("127.0.0.1:8080")
311    ///     .document_root(".")
312    ///     .build();
313    /// assert!(ok.is_ok());
314    /// ```
315    ///
316    /// # Errors
317    ///
318    /// Returns `Err` when:
319    /// - the address was not provided.
320    /// - the document root was not provided.
321    ///
322    /// # Panics
323    ///
324    /// This function does not panic.
325    #[doc(alias = "finalize")]
326    pub fn build(self) -> Result<Server, &'static str> {
327        let address = self.address.ok_or("Address is required")?;
328        let document_root =
329            self.document_root.ok_or("Document root is required")?;
330        // Canonicalize once at build time so the request hot path no longer
331        // issues two fs::canonicalize syscalls per request.
332        let canonical_document_root = fs::canonicalize(&document_root)
333            .unwrap_or_else(|_| document_root.clone());
334
335        Ok(Server {
336            address,
337            document_root,
338            canonical_document_root,
339            cors_enabled: self.cors_enabled,
340            cors_origins: self.cors_origins,
341            custom_headers: self.custom_headers,
342            request_timeout: self.request_timeout,
343            connection_timeout: self.connection_timeout,
344            rate_limit_per_minute: self.rate_limit_per_minute,
345            static_cache_ttl_secs: self.static_cache_ttl_secs,
346            max_buffered_body_bytes: self.max_buffered_body_bytes,
347        })
348    }
349}
350
351/// Holds shutdown state and coordination for graceful server termination
352#[derive(Debug, Clone)]
353pub struct ShutdownSignal {
354    /// Flag indicating if shutdown has been requested
355    pub should_shutdown: Arc<AtomicBool>,
356    /// Counter tracking active connections
357    pub active_connections: Arc<AtomicUsize>,
358    /// Maximum time to wait for connections to drain during shutdown
359    pub shutdown_timeout: Duration,
360}
361
362impl Default for ShutdownSignal {
363    fn default() -> Self {
364        Self::new(Duration::from_secs(30))
365    }
366}
367
368impl ShutdownSignal {
369    /// Creates a new shutdown signal with the specified timeout
370    pub fn new(shutdown_timeout: Duration) -> Self {
371        Self {
372            should_shutdown: Arc::new(AtomicBool::new(false)),
373            active_connections: Arc::new(AtomicUsize::new(0)),
374            shutdown_timeout,
375        }
376    }
377
378    /// Signals that shutdown should begin
379    pub fn shutdown(&self) {
380        self.should_shutdown.store(true, Ordering::SeqCst);
381        println!(
382            "🛑 Shutdown signal received. Waiting for active connections to finish..."
383        );
384    }
385
386    /// Check if shutdown has been requested
387    pub fn is_shutdown_requested(&self) -> bool {
388        self.should_shutdown.load(Ordering::SeqCst)
389    }
390
391    /// Increment the active connection counter
392    pub fn connection_started(&self) {
393        let _ = self.active_connections.fetch_add(1, Ordering::SeqCst);
394    }
395
396    /// Decrement the active connection counter
397    pub fn connection_finished(&self) {
398        let _ = self.active_connections.fetch_sub(1, Ordering::SeqCst);
399    }
400
401    /// Get the current number of active connections
402    pub fn active_connection_count(&self) -> usize {
403        self.active_connections.load(Ordering::SeqCst)
404    }
405
406    /// Wait for all connections to drain or timeout to expire
407    pub fn wait_for_shutdown(&self) -> bool {
408        let start_time = Instant::now();
409
410        while self.active_connection_count() > 0
411            && start_time.elapsed() < self.shutdown_timeout
412        {
413            let remaining = self
414                .shutdown_timeout
415                .saturating_sub(start_time.elapsed());
416            println!(
417                "⏳ Waiting for {} active connection(s) to finish... ({:.1}s remaining)",
418                self.active_connection_count(),
419                remaining.as_secs_f32()
420            );
421
422            // Sleep in short intervals to avoid overshooting small timeouts.
423            thread::sleep(remaining.min(Duration::from_millis(50)));
424        }
425
426        let remaining_connections = self.active_connection_count();
427        if remaining_connections > 0 {
428            println!(
429                "⚠️  Shutdown timeout reached. {} connection(s) will be forcibly terminated.",
430                remaining_connections
431            );
432            false
433        } else {
434            println!("✅ All connections closed gracefully.");
435            true
436        }
437    }
438}
439
440/// A simple thread pool for handling concurrent connections efficiently
441pub struct ThreadPool {
442    workers: Vec<Worker>,
443    sender: Sender<Job>,
444}
445
446impl std::fmt::Debug for ThreadPool {
447    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
448        f.debug_struct("ThreadPool")
449            .field("workers", &self.workers)
450            .field("sender", &"<Sender<Job>>")
451            .finish()
452    }
453}
454
455/// Represents a job that can be executed by the thread pool
456type Job = Box<dyn FnOnce() + Send + 'static>;
457
458/// A worker thread that processes jobs from the thread pool queue
459#[derive(Debug)]
460struct Worker {
461    id: usize,
462    thread: Option<thread::JoinHandle<()>>,
463}
464
465impl ThreadPool {
466    /// Creates a new ThreadPool with the specified number of threads.
467    ///
468    /// # Arguments
469    /// * `size` - The number of threads in the pool
470    ///
471    /// # Panics
472    /// The `new` function will panic if the size is zero.
473    pub fn new(size: usize) -> ThreadPool {
474        assert!(size > 0);
475
476        // crossbeam-channel's MPMC receiver is Clone + lock-free, so each
477        // worker owns its own handle to the shared queue instead of
478        // contending on Arc<Mutex<Receiver>>. Previous design serialized
479        // every `recv()` through one mutex regardless of worker count.
480        let (sender, receiver) = unbounded();
481
482        let mut workers = Vec::with_capacity(size);
483
484        for id in 0..size {
485            workers.push(Worker::new(id, receiver.clone()));
486        }
487
488        // Return configured thread_pool instance
489        ThreadPool { workers, sender }
490    }
491
492    /// Execute a job on the thread pool.
493    ///
494    /// # Arguments
495    /// * `f` - The closure to execute
496    pub fn execute<F>(&self, f: F)
497    where
498        F: FnOnce() + Send + 'static,
499    {
500        let job = Box::new(f);
501        self.sender.send(job).unwrap();
502    }
503}
504
505impl Drop for ThreadPool {
506    fn drop(&mut self) {
507        // Close the job channel first so workers exit their recv() loop.
508        // Replacing the sender drops the original; when the last Sender
509        // goes out of scope the channel disconnects and every cloned
510        // Receiver returns Err from recv().
511        let (replacement_sender, _replacement_receiver) = unbounded();
512        let old_sender =
513            std::mem::replace(&mut self.sender, replacement_sender);
514        drop(old_sender);
515
516        for worker in &mut self.workers {
517            println!("Shutting down worker {}", worker.id);
518
519            if let Some(thread) = worker.thread.take() {
520                thread.join().unwrap();
521            }
522        }
523    }
524}
525
526impl Worker {
527    fn new(id: usize, receiver: Receiver<Job>) -> Worker {
528        let thread = thread::spawn(move || {
529            while let Ok(job) = receiver.recv() {
530                job();
531            }
532            println!("Worker {id} disconnected; shutting down.");
533        });
534
535        Worker {
536            id,
537            thread: Some(thread),
538        }
539    }
540}
541
542/// Holds the connection pool state for managing database or external connections
543#[derive(Debug)]
544pub struct ConnectionPool {
545    max_connections: usize,
546    active_connections: Arc<AtomicUsize>,
547}
548
549impl ConnectionPool {
550    /// Creates a new connection pool with the specified maximum connections
551    pub fn new(max_connections: usize) -> Self {
552        // Initialize connection_pool with bounded resources
553        Self {
554            max_connections,
555            active_connections: Arc::new(AtomicUsize::new(0)),
556        }
557    }
558
559    /// Attempts to acquire a connection from the pool
560    pub fn acquire(&self) -> Result<ConnectionGuard, io::Error> {
561        let mut current =
562            self.active_connections.load(Ordering::SeqCst);
563        let reserved = loop {
564            if current >= self.max_connections {
565                break Err(());
566            }
567            match self.active_connections.compare_exchange_weak(
568                current,
569                current + 1,
570                Ordering::SeqCst,
571                Ordering::SeqCst,
572            ) {
573                Ok(_) => break Ok(()),
574                Err(actual) => current = actual,
575            }
576        };
577        if reserved.is_err() {
578            return Err(io::Error::new(
579                io::ErrorKind::WouldBlock,
580                "Connection pool exhausted",
581            ));
582        }
583        Ok(ConnectionGuard {
584            pool: Arc::clone(&self.active_connections),
585        })
586    }
587
588    /// Returns the current number of active connections
589    pub fn active_count(&self) -> usize {
590        self.active_connections.load(Ordering::SeqCst)
591    }
592}
593
594/// RAII guard for connection pool resources
595#[derive(Debug)]
596pub struct ConnectionGuard {
597    pool: Arc<AtomicUsize>,
598}
599
600impl Drop for ConnectionGuard {
601    fn drop(&mut self) {
602        let _ = self.pool.fetch_sub(1, Ordering::SeqCst);
603    }
604}
605
606impl Server {
607    /// Creates a server using the minimal required configuration.
608    ///
609    /// Use this constructor when you want a quick default path. For advanced runtime
610    /// policy, prefer [`Server::builder`].
611    ///
612    /// # Examples
613    ///
614    /// ```rust
615    /// use http_handle::Server;
616    ///
617    /// let server = Server::new("127.0.0.1:8080", ".");
618    /// assert_eq!(server.address(), "127.0.0.1:8080");
619    /// ```
620    ///
621    /// # Panics
622    ///
623    /// This function does not panic.
624    #[doc(alias = "constructor")]
625    pub fn new(address: &str, document_root: &str) -> Self {
626        let document_root = PathBuf::from(document_root);
627        let canonical_document_root = fs::canonicalize(&document_root)
628            .unwrap_or_else(|_| document_root.clone());
629        Server {
630            address: address.to_string(),
631            document_root,
632            canonical_document_root,
633            cors_enabled: None,
634            cors_origins: None,
635            custom_headers: None,
636            request_timeout: None,
637            connection_timeout: None,
638            rate_limit_per_minute: None,
639            static_cache_ttl_secs: None,
640            max_buffered_body_bytes: None,
641        }
642    }
643
644    /// Returns a fluent builder for optional server policies.
645    ///
646    /// # Examples
647    ///
648    /// ```rust
649    /// use http_handle::Server;
650    ///
651    /// let server = Server::builder()
652    ///     .address("127.0.0.1:8080")
653    ///     .document_root(".")
654    ///     .build()
655    ///     .expect("builder should succeed");
656    /// assert_eq!(server.address(), "127.0.0.1:8080");
657    /// ```
658    ///
659    /// # Panics
660    ///
661    /// This function does not panic.
662    pub fn builder() -> ServerBuilder {
663        ServerBuilder::new()
664    }
665
666    /// Starts a blocking HTTP/1.1 listener loop.
667    ///
668    /// On Linux, macOS, and Windows, this binds a `TcpListener` and accepts connections
669    /// in a thread-per-connection model.
670    ///
671    /// # Examples
672    ///
673    /// ```rust,no_run
674    /// use http_handle::Server;
675    ///
676    /// let server = Server::new("127.0.0.1:8080", ".");
677    /// let _ = server.start();
678    /// ```
679    ///
680    /// # Errors
681    ///
682    /// Returns `Err` if binding fails or the listener cannot be configured.
683    ///
684    /// # Panics
685    ///
686    /// This function does not intentionally panic.
687    #[doc(alias = "listen")]
688    #[doc(alias = "serve")]
689    pub fn start(&self) -> io::Result<()> {
690        let listener = TcpListener::bind(&self.address)?;
691        println!("❯ Server is now running at http://{}", self.address);
692        println!("  Document root: {}", self.document_root.display());
693        println!("  Press Ctrl+C to stop the server.");
694
695        Self::run_basic_accept_loop(listener.incoming(), self.clone());
696
697        Ok(())
698    }
699
700    /// Starts the server with OS-signal-aware graceful shutdown.
701    ///
702    /// On macOS/Linux, this responds to `SIGINT`/`SIGTERM` via the installed signal handler.
703    /// On Windows, `Ctrl+C` triggers equivalent shutdown behavior through the same handler API.
704    ///
705    /// # Examples
706    ///
707    /// ```rust,no_run
708    /// use http_handle::Server;
709    /// use std::time::Duration;
710    ///
711    /// let server = Server::new("127.0.0.1:8080", ".");
712    /// let _ = server.start_with_graceful_shutdown(Duration::from_secs(5));
713    /// ```
714    ///
715    /// # Errors
716    ///
717    /// Returns `Err` when binding or socket configuration fails.
718    ///
719    /// # Panics
720    ///
721    /// This function does not intentionally panic.
722    #[doc(alias = "graceful shutdown")]
723    pub fn start_with_graceful_shutdown(
724        &self,
725        shutdown_timeout: Duration,
726    ) -> io::Result<()> {
727        let shutdown = Arc::new(ShutdownSignal::new(shutdown_timeout));
728        self.start_with_shutdown_signal(shutdown)
729    }
730
731    /// Starts the server with caller-managed shutdown coordination.
732    ///
733    /// # Examples
734    ///
735    /// ```rust,no_run
736    /// use http_handle::{Server, ShutdownSignal};
737    /// use std::sync::Arc;
738    /// use std::time::Duration;
739    ///
740    /// let server = Server::new("127.0.0.1:8080", ".");
741    /// let signal = Arc::new(ShutdownSignal::new(Duration::from_secs(2)));
742    /// let _ = server.start_with_shutdown_signal(signal);
743    /// ```
744    ///
745    /// # Errors
746    ///
747    /// Returns `Err` when binding or listener configuration fails.
748    ///
749    /// # Panics
750    ///
751    /// This function does not intentionally panic.
752    #[doc(alias = "shutdown signal")]
753    pub fn start_with_shutdown_signal(
754        &self,
755        shutdown: Arc<ShutdownSignal>,
756    ) -> io::Result<()> {
757        self.start_with_shutdown_signal_and_ready(shutdown, |_| {})
758    }
759
760    /// Starts the server with a shutdown signal and reports the actual bound address.
761    ///
762    /// This is useful when binding to port `0` in tests and callers need the kernel-assigned
763    /// port before sending requests.
764    ///
765    /// # Arguments
766    ///
767    /// * `shutdown` - The shutdown signal to coordinate graceful termination
768    /// * `on_ready` - Callback invoked once with the actual bound `ip:port`
769    ///
770    /// # Returns
771    ///
772    /// A `Result` indicating success or an I/O error.
773    pub fn start_with_shutdown_signal_and_ready<F>(
774        &self,
775        shutdown: Arc<ShutdownSignal>,
776        on_ready: F,
777    ) -> io::Result<()>
778    where
779        F: FnOnce(String),
780    {
781        // Install signal handlers
782        Self::install_signal_handlers(shutdown.clone());
783
784        let listener = TcpListener::bind(&self.address)?;
785        let bound_address = listener.local_addr()?.to_string();
786        on_ready(bound_address.clone());
787        println!("❯ Server is now running at http://{}", bound_address);
788        println!("  Document root: {}", self.document_root.display());
789        println!("  Press Ctrl+C to stop the server gracefully.");
790
791        // Set a short timeout on the listener to allow checking shutdown signal
792        listener.set_nonblocking(true)?;
793
794        // Adaptive backoff between non-blocking accept polls. Starts at
795        // 100 µs so a connection arriving while idle waits at most that
796        // long before we accept it; doubles up to a 5 ms cap so a truly
797        // idle server polls ~200×/sec (negligible CPU) and shutdown
798        // detection latency is bounded at 5 ms instead of 100 ms.
799        const MIN_IDLE_SLEEP: Duration = Duration::from_micros(100);
800        const MAX_IDLE_SLEEP: Duration = Duration::from_millis(5);
801        let mut idle_sleep = MIN_IDLE_SLEEP;
802
803        loop {
804            // Check if shutdown was requested
805            if shutdown.is_shutdown_requested() {
806                println!(
807                    "🛑 Shutdown requested. Stopping new connections..."
808                );
809                break;
810            }
811
812            match listener.accept() {
813                Ok((stream, _addr)) => {
814                    idle_sleep = MIN_IDLE_SLEEP;
815                    Self::run_tracked_accept(
816                        stream,
817                        self.clone(),
818                        shutdown.clone(),
819                    );
820                }
821                Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
822                    thread::sleep(idle_sleep);
823                    idle_sleep = (idle_sleep * 2).min(MAX_IDLE_SLEEP);
824                }
825                Err(e) => Self::log_listener_error(e),
826            }
827        }
828
829        // Wait for existing connections to finish
830        let graceful = shutdown.wait_for_shutdown();
831
832        if graceful {
833            println!("✅ Server shut down gracefully.");
834        } else {
835            println!(
836                "⚠️  Server shut down with active connections remaining."
837            );
838        }
839
840        Ok(())
841    }
842
843    /// Installs signal handlers for graceful shutdown
844    ///
845    /// # Arguments
846    ///
847    /// * `shutdown` - The shutdown signal to trigger when signals are received
848    fn install_signal_handlers(shutdown: Arc<ShutdownSignal>) {
849        let slot =
850            SHUTDOWN_SIGNAL_SLOT.get_or_init(|| Mutex::new(None));
851
852        // Update the active shutdown signal for this server run.
853        if let Ok(mut guard) = slot.lock() {
854            *guard = Some(shutdown);
855        }
856
857        // Register the OS signal handler once per process.
858        SIGNAL_HANDLER_INSTALL.call_once(|| {
859            let _ = ctrlc::set_handler(Self::handle_shutdown_signal);
860        });
861    }
862
863    fn handle_shutdown_signal() {
864        if let Some(slot) = SHUTDOWN_SIGNAL_SLOT.get() {
865            Self::trigger_shutdown_from_slot(slot);
866        }
867    }
868
869    fn trigger_shutdown_from_slot(
870        slot: &Mutex<Option<Arc<ShutdownSignal>>>,
871    ) {
872        if let Ok(guard) = slot.lock()
873            && let Some(shutdown_signal) = guard.as_ref()
874        {
875            shutdown_signal.shutdown();
876        }
877    }
878
879    /// Starts the server with thread pooling for better resource management under load.
880    ///
881    /// This method uses a fixed-size thread pool to handle connections, preventing
882    /// resource exhaustion under high load by limiting the number of concurrent threads.
883    ///
884    /// # Arguments
885    ///
886    /// * `thread_pool_size` - The number of worker threads in the pool
887    ///
888    /// # Returns
889    ///
890    /// A `Result` indicating success or an I/O error.
891    pub fn start_with_thread_pool(
892        &self,
893        thread_pool_size: usize,
894    ) -> io::Result<()> {
895        let thread_pool = ThreadPool::new(thread_pool_size);
896        let listener = TcpListener::bind(&self.address)?;
897
898        println!("❯ Server is now running at http://{}", self.address);
899        println!("  Document root: {}", self.document_root.display());
900        println!("  Thread pool size: {} workers", thread_pool_size);
901        println!("  Press Ctrl+C to stop the server.");
902
903        Self::run_thread_pool_accept_loop(
904            listener.incoming(),
905            self.clone(),
906            &thread_pool,
907        );
908
909        Ok(())
910    }
911
912    /// Starts the server with both thread pooling and connection pooling for optimal resource management.
913    ///
914    /// This method provides the highest level of resource control by combining:
915    /// - Fixed-size thread pool to limit concurrent worker threads
916    /// - Connection pool to limit the number of simultaneously processed connections
917    /// - Graceful degradation when limits are reached
918    ///
919    /// # Arguments
920    ///
921    /// * `thread_pool_size` - The number of worker threads in the pool
922    /// * `max_connections` - The maximum number of concurrent connections to process
923    ///
924    /// # Returns
925    ///
926    /// A `Result` indicating success or an I/O error.
927    pub fn start_with_pooling(
928        &self,
929        thread_pool_size: usize,
930        max_connections: usize,
931    ) -> io::Result<()> {
932        let thread_pool = ThreadPool::new(thread_pool_size);
933        let connection_pool =
934            Arc::new(ConnectionPool::new(max_connections));
935        let listener = TcpListener::bind(&self.address)?;
936
937        println!("❯ Server is now running at http://{}", self.address);
938        println!("  Document root: {}", self.document_root.display());
939        println!("  Thread pool size: {} workers", thread_pool_size);
940        println!("  Max concurrent connections: {}", max_connections);
941        println!("  Press Ctrl+C to stop the server.");
942
943        Self::run_pooling_accept_loop(
944            listener.incoming(),
945            self.clone(),
946            &thread_pool,
947            connection_pool,
948        );
949
950        Ok(())
951    }
952
953    fn log_connection_result(result: Result<(), ServerError>) {
954        if let Err(error) = result {
955            eprintln!("Error handling connection: {}", error);
956        }
957    }
958
959    fn log_listener_error(error: io::Error) {
960        eprintln!("Connection error: {}", error);
961    }
962
963    fn run_tracked_accept(
964        stream: TcpStream,
965        server: Server,
966        shutdown: Arc<ShutdownSignal>,
967    ) {
968        shutdown.connection_started();
969        let _ = thread::spawn(move || {
970            let result =
971                handle_connection_tracked(stream, &server, &shutdown);
972            shutdown.connection_finished();
973            Self::log_connection_result(result);
974        });
975    }
976
977    fn run_basic_accept_loop<I>(incoming: I, server: Server)
978    where
979        I: IntoIterator<Item = io::Result<TcpStream>>,
980    {
981        for stream in incoming {
982            match stream {
983                Ok(stream) => {
984                    let server = server.clone();
985                    let _ = thread::spawn(move || {
986                        Self::log_connection_result(handle_connection(
987                            stream, &server,
988                        ));
989                    });
990                }
991                Err(error) => Self::log_listener_error(error),
992            }
993        }
994    }
995
996    fn run_thread_pool_accept_loop<I>(
997        incoming: I,
998        server: Server,
999        thread_pool: &ThreadPool,
1000    ) where
1001        I: IntoIterator<Item = io::Result<TcpStream>>,
1002    {
1003        for stream in incoming {
1004            match stream {
1005                Ok(stream) => {
1006                    let server = server.clone();
1007                    thread_pool.execute(move || {
1008                        Self::log_connection_result(handle_connection(
1009                            stream, &server,
1010                        ));
1011                    });
1012                }
1013                Err(error) => Self::log_listener_error(error),
1014            }
1015        }
1016    }
1017
1018    fn run_pooling_accept_loop<I>(
1019        incoming: I,
1020        server: Server,
1021        thread_pool: &ThreadPool,
1022        connection_pool: Arc<ConnectionPool>,
1023    ) where
1024        I: IntoIterator<Item = io::Result<TcpStream>>,
1025    {
1026        for stream in incoming {
1027            match stream {
1028                Ok(stream) => {
1029                    let server = server.clone();
1030                    let pool_clone = Arc::clone(&connection_pool);
1031                    thread_pool.execute(move || match pool_clone.acquire() {
1032                        Ok(_guard) => Self::log_connection_result(
1033                            handle_connection(stream, &server),
1034                        ),
1035                        Err(_) => {
1036                            if let Err(error) =
1037                                send_service_unavailable(stream)
1038                            {
1039                                eprintln!(
1040                                    "Error sending service unavailable: {}",
1041                                    error
1042                                );
1043                            }
1044                        }
1045                    });
1046                }
1047                Err(error) => Self::log_listener_error(error),
1048            }
1049        }
1050    }
1051
1052    // Getter methods for configuration fields (needed for testing)
1053
1054    /// Returns the CORS enabled setting
1055    pub fn cors_enabled(&self) -> Option<bool> {
1056        self.cors_enabled
1057    }
1058
1059    /// Returns the CORS origins setting
1060    pub fn cors_origins(&self) -> &Option<Vec<String>> {
1061        &self.cors_origins
1062    }
1063
1064    /// Returns the custom headers setting
1065    pub fn custom_headers(&self) -> &Option<HashMap<String, String>> {
1066        &self.custom_headers
1067    }
1068
1069    /// Returns the request timeout setting
1070    pub fn request_timeout(&self) -> Option<Duration> {
1071        self.request_timeout
1072    }
1073
1074    /// Returns the connection timeout setting
1075    pub fn connection_timeout(&self) -> Option<Duration> {
1076        self.connection_timeout
1077    }
1078
1079    /// Returns the server address
1080    pub fn address(&self) -> &str {
1081        &self.address
1082    }
1083
1084    /// Returns the document root path
1085    pub fn document_root(&self) -> &PathBuf {
1086        &self.document_root
1087    }
1088
1089    /// Returns the canonical document root, cached at build time.
1090    ///
1091    /// Pre-canonicalised so the request hot path can do a prefix check
1092    /// without issuing `fs::canonicalize` per request. Falls back to
1093    /// `document_root` when canonicalisation produced an empty path
1094    /// (e.g. the path was constructed via `Default` and never built).
1095    pub fn canonical_document_root(&self) -> &Path {
1096        if self.canonical_document_root.as_os_str().is_empty() {
1097            &self.document_root
1098        } else {
1099            &self.canonical_document_root
1100        }
1101    }
1102
1103    /// Returns the configured maximum file size, in bytes, that the
1104    /// in-memory file-serve path will buffer. Falls back to
1105    /// [`DEFAULT_MAX_BUFFERED_BODY_BYTES`] when the builder didn't
1106    /// override it.
1107    pub fn max_buffered_body_bytes(&self) -> u64 {
1108        self.max_buffered_body_bytes
1109            .unwrap_or(DEFAULT_MAX_BUFFERED_BODY_BYTES)
1110    }
1111}
1112
1113/// Sends a 503 Service Unavailable response when connection pool is exhausted.
1114///
1115/// # Arguments
1116///
1117/// * `mut stream` - The TCP stream to send the response to
1118///
1119/// # Returns
1120///
1121/// A `Result` indicating success or an I/O error.
1122fn send_service_unavailable(mut stream: TcpStream) -> io::Result<()> {
1123    let mut response = Response::new(
1124        503,
1125        "SERVICE UNAVAILABLE",
1126        b"Service temporarily unavailable. Please try again later."
1127            .to_vec(),
1128    );
1129
1130    response.add_header("Content-Type", "text/plain");
1131    response.add_header("Retry-After", "1"); // Suggest client retry after 1 second
1132    response.add_header("Connection", "close");
1133
1134    response.send(&mut stream).map_err(|e| {
1135        use std::io::Error;
1136        Error::other(format!("Failed to send response: {}", e))
1137    })?;
1138    Ok(())
1139}
1140
1141/// Maximum number of HTTP/1.1 requests served on a single keep-alive
1142/// connection before the server forces a close. Bounds resource use
1143/// per client and prevents a single connection from monopolising a
1144/// worker indefinitely.
1145pub(crate) const MAX_KEEPALIVE_REQUESTS: usize = 100;
1146
1147/// Idle timeout applied to the read side of a kept-alive connection
1148/// while waiting for the next request. Smaller than the per-request
1149/// timeout so an idle client is reaped promptly without affecting the
1150/// time budget for a request that's actually in flight. Industry
1151/// defaults sit at 5–15 s; 5 s is a reasonable middle ground.
1152pub(crate) const KEEPALIVE_IDLE_TIMEOUT: Duration =
1153    Duration::from_secs(5);
1154
1155/// Default ceiling for the in-memory `serve_file_response` path when
1156/// `Server::max_buffered_body_bytes` isn't set explicitly. Files
1157/// larger than this cap are rejected with `503 Service Unavailable`
1158/// rather than allowed to drive the server's RSS to N × file_size on
1159/// N concurrent requests.
1160///
1161/// Truly large files need a streaming response path that writes
1162/// directly to the wire instead of materialising a `Vec<u8>` body.
1163/// `streaming::ChunkStream` provides the building block; wiring it
1164/// into `Response` requires a `ResponseBody` enum which is a public
1165/// API change parked for v0.1.
1166///
1167/// 64 MiB covers the vast majority of static-asset workloads (HTML,
1168/// JS bundles, images, small video previews). Override per-deployment
1169/// via [`ServerBuilder::max_buffered_body_bytes`].
1170pub const DEFAULT_MAX_BUFFERED_BODY_BYTES: u64 = 64 * 1024 * 1024;
1171
1172/// Connection lifecycle decision derived from the request and HTTP
1173/// version per RFC 7230 §6.3:
1174///
1175/// * HTTP/1.1: keep-alive by default; close on explicit `Connection: close`.
1176/// * HTTP/1.0: close by default; keep-alive only on explicit
1177///   `Connection: keep-alive`.
1178///
1179/// Errors and missing-parse cases always close.
1180#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1181pub(crate) enum ConnectionPolicy {
1182    KeepAlive,
1183    Close,
1184}
1185
1186impl ConnectionPolicy {
1187    pub(crate) fn header_value(self) -> &'static str {
1188        match self {
1189            ConnectionPolicy::KeepAlive => "keep-alive",
1190            ConnectionPolicy::Close => "close",
1191        }
1192    }
1193
1194    pub(crate) fn from_request(request: &Request) -> Self {
1195        let connection_header = request
1196            .header("connection")
1197            .map(|h| h.trim().to_ascii_lowercase());
1198        match request.version() {
1199            "HTTP/1.1" => match connection_header.as_deref() {
1200                Some("close") => ConnectionPolicy::Close,
1201                _ => ConnectionPolicy::KeepAlive,
1202            },
1203            _ => match connection_header.as_deref() {
1204                Some("keep-alive") => ConnectionPolicy::KeepAlive,
1205                _ => ConnectionPolicy::Close,
1206            },
1207        }
1208    }
1209}
1210
1211/// Handles a single client connection.
1212///
1213/// Implements HTTP/1.1 persistent connections (keep-alive) per RFC 7230:
1214/// the loop reads requests on the same TCP stream until the client (or
1215/// the server's response policy) signals close, an idle/read timeout
1216/// fires, or [`MAX_KEEPALIVE_REQUESTS`] is reached.
1217///
1218/// # Arguments
1219///
1220/// * `stream` - A `TcpStream` representing the client connection.
1221/// * `server` - The configured `Server`, used for policy and document root.
1222///
1223/// # Returns
1224///
1225/// A `Result` indicating success or a `ServerError`.
1226pub(crate) fn handle_connection(
1227    mut stream: TcpStream,
1228    server: &Server,
1229) -> Result<(), ServerError> {
1230    // Disable Nagle so small responses ship immediately instead of
1231    // stalling behind delayed-ACK on the client side.
1232    let _ = stream.set_nodelay(true);
1233    let timeout =
1234        server.request_timeout.unwrap_or(Duration::from_secs(30));
1235    stream.set_read_timeout(Some(timeout))?;
1236    stream.set_write_timeout(Some(timeout))?;
1237
1238    let peer_ip = stream.peer_addr().ok().map(|addr| addr.ip());
1239
1240    for i in 0..MAX_KEEPALIVE_REQUESTS {
1241        if i > 0 {
1242            // Tighten the read side for subsequent requests on a
1243            // persistent connection. The first request is free to use
1244            // the configured request_timeout; an idle client between
1245            // requests is reaped on KEEPALIVE_IDLE_TIMEOUT.
1246            stream.set_read_timeout(Some(KEEPALIVE_IDLE_TIMEOUT))?;
1247        }
1248        let (mut response, policy) =
1249            build_response_for_stream(server, &stream, peer_ip);
1250        // Authoritative Connection header on the response. Overwrites
1251        // anything `apply_response_policies` may have set; the policy
1252        // decision sits closer to the wire than per-handler choices.
1253        response.set_connection_header(policy.header_value());
1254
1255        if response.send(&mut stream).is_err() {
1256            // Peer hung up mid-write — the keep-alive loop is over.
1257            return Ok(());
1258        }
1259        if policy == ConnectionPolicy::Close {
1260            return Ok(());
1261        }
1262    }
1263    Ok(())
1264}
1265
1266/// Handles a single client connection with shutdown signal awareness.
1267///
1268/// This function is similar to `handle_connection` but can be interrupted
1269/// during shutdown sequences.
1270///
1271/// # Arguments
1272///
1273/// * `stream` - A `TcpStream` representing the client connection.
1274/// * `document_root` - A `Path` representing the server's document root.
1275/// * `shutdown` - The shutdown signal for coordination
1276///
1277/// # Returns
1278///
1279/// A `Result` indicating success or a `ServerError`.
1280fn handle_connection_tracked(
1281    mut stream: TcpStream,
1282    server: &Server,
1283    _shutdown: &ShutdownSignal,
1284) -> Result<(), ServerError> {
1285    // Ensure per-connection reads are blocking even if the listener is non-blocking.
1286    stream.set_nonblocking(false)?;
1287    // Disable Nagle — small responses should not wait for delayed ACKs.
1288    let _ = stream.set_nodelay(true);
1289
1290    // Set a reasonable timeout for connection handling
1291    let timeout =
1292        server.connection_timeout.unwrap_or(Duration::from_secs(30));
1293    stream.set_read_timeout(Some(timeout))?;
1294    stream.set_write_timeout(Some(timeout))?;
1295
1296    let peer_ip = stream.peer_addr().ok().map(|addr| addr.ip());
1297
1298    for i in 0..MAX_KEEPALIVE_REQUESTS {
1299        if i > 0 {
1300            stream.set_read_timeout(Some(KEEPALIVE_IDLE_TIMEOUT))?;
1301        }
1302        let (mut response, policy) =
1303            build_response_for_stream(server, &stream, peer_ip);
1304        response.set_connection_header(policy.header_value());
1305        if response.send(&mut stream).is_err() {
1306            return Ok(());
1307        }
1308        if policy == ConnectionPolicy::Close {
1309            return Ok(());
1310        }
1311    }
1312    Ok(())
1313}
1314
1315fn build_response_for_stream(
1316    server: &Server,
1317    stream: &TcpStream,
1318    peer_ip: Option<IpAddr>,
1319) -> (Response, ConnectionPolicy) {
1320    match Request::from_stream(stream) {
1321        Ok(request) => {
1322            // Capture the keep-alive decision before consuming the
1323            // request: rate-limited or metrics responses still honour
1324            // the client's stated preference. Errors below override
1325            // to Close because the parsing path was destabilised.
1326            let policy = ConnectionPolicy::from_request(&request);
1327            if request.path() == "/metrics" && request.method() == "GET"
1328            {
1329                return (generate_metrics_response(), policy);
1330            }
1331            if let Some(ip) = peer_ip
1332                && is_rate_limited(server, ip)
1333            {
1334                let _ =
1335                    METRIC_RATE_LIMITED.fetch_add(1, Ordering::Relaxed);
1336                return (generate_too_many_requests_response(), policy);
1337            }
1338            (
1339                build_response_for_request_with_metrics(
1340                    server, &request,
1341                ),
1342                policy,
1343            )
1344        }
1345        Err(error) => (
1346            response_from_error(&error, &server.document_root),
1347            ConnectionPolicy::Close,
1348        ),
1349    }
1350}
1351
1352/// Builds a response for an already parsed request and records response metrics.
1353///
1354/// This is shared by protocol-specific frontends (for example HTTP/1 and HTTP/2)
1355/// to keep behavior consistent across server entrypoints.
1356pub(crate) fn build_response_for_request_with_metrics(
1357    server: &Server,
1358    request: &Request,
1359) -> Response {
1360    let response = build_response_for_request(server, request);
1361    record_metrics(&response);
1362    response
1363}
1364
1365/// Builds a response for an already parsed request and applies server policies.
1366pub(crate) fn build_response_for_request(
1367    server: &Server,
1368    request: &Request,
1369) -> Response {
1370    let generated = match request.method() {
1371        "GET" => generate_response_with_cache(
1372            request,
1373            &server.document_root,
1374            &server.canonical_document_root,
1375            server.static_cache_ttl_secs,
1376            server.max_buffered_body_bytes(),
1377        ),
1378        "HEAD" => {
1379            generate_head_response(request, &server.document_root)
1380        }
1381        "OPTIONS" => generate_options_response(request),
1382        _ => Ok(generate_method_not_allowed_response()),
1383    };
1384    match generated {
1385        Ok(response) => {
1386            apply_response_policies(response, server, request)
1387        }
1388        Err(error) => {
1389            response_from_error(&error, &server.document_root)
1390        }
1391    }
1392}
1393
1394fn record_metrics(response: &Response) {
1395    let _ = METRIC_REQUESTS_TOTAL.fetch_add(1, Ordering::Relaxed);
1396    if (400..500).contains(&response.status_code) {
1397        let _ = METRIC_RESPONSES_4XX.fetch_add(1, Ordering::Relaxed);
1398    } else if response.status_code >= 500 {
1399        let _ = METRIC_RESPONSES_5XX.fetch_add(1, Ordering::Relaxed);
1400    }
1401}
1402
1403fn generate_metrics_response() -> Response {
1404    let body = format!(
1405        "http_handle_requests_total {}\nhttp_handle_responses_4xx_total {}\nhttp_handle_responses_5xx_total {}\nhttp_handle_rate_limited_total {}\n",
1406        METRIC_REQUESTS_TOTAL.load(Ordering::Relaxed),
1407        METRIC_RESPONSES_4XX.load(Ordering::Relaxed),
1408        METRIC_RESPONSES_5XX.load(Ordering::Relaxed),
1409        METRIC_RATE_LIMITED.load(Ordering::Relaxed),
1410    );
1411    let mut response = Response::new(200, "OK", body.into_bytes());
1412    response.add_header("Content-Type", "text/plain; version=0.0.3");
1413    response
1414}
1415
1416fn generate_too_many_requests_response() -> Response {
1417    let mut response = Response::new(
1418        429,
1419        "TOO MANY REQUESTS",
1420        b"Rate limit exceeded".to_vec(),
1421    );
1422    response.add_header("Content-Type", "text/plain");
1423    response.add_header("Retry-After", "60");
1424    response
1425}
1426
1427fn rate_limit_shard_index(ip: IpAddr) -> usize {
1428    use std::collections::hash_map::DefaultHasher;
1429    use std::hash::{Hash, Hasher};
1430    let mut h = DefaultHasher::new();
1431    ip.hash(&mut h);
1432    (h.finish() as usize) & (RATE_LIMIT_SHARDS - 1)
1433}
1434
1435fn rate_limit_table() -> &'static RateLimitTable {
1436    RATE_LIMIT_STATE.get_or_init(|| {
1437        std::array::from_fn(|_| Mutex::new(HashMap::new()))
1438    })
1439}
1440
1441fn is_rate_limited(server: &Server, ip: IpAddr) -> bool {
1442    let Some(limit) = server.rate_limit_per_minute else {
1443        return false;
1444    };
1445    let now = Instant::now();
1446    // Sharded by IP hash so concurrent requests from distinct clients
1447    // almost always land on different shards. Cuts effective contention
1448    // by a factor of RATE_LIMIT_SHARDS without introducing a dependency.
1449    let shard = &rate_limit_table()[rate_limit_shard_index(ip)];
1450    let mut guard = match shard.lock() {
1451        Ok(guard) => guard,
1452        Err(poisoned) => poisoned.into_inner(),
1453    };
1454    let hits = guard.entry(ip).or_default();
1455    hits.retain(|timestamp| {
1456        now.duration_since(*timestamp) <= Duration::from_secs(60)
1457    });
1458    if hits.len() >= limit {
1459        return true;
1460    }
1461    hits.push(now);
1462    false
1463}
1464
1465/// Generates an HTTP response based on the requested file.
1466///
1467/// # Arguments
1468///
1469/// * `request` - A `Request` instance representing the client's request.
1470/// * `document_root` - A `Path` representing the server's document root.
1471///
1472/// # Returns
1473///
1474/// A `Result` containing the `Response` or a `ServerError`.
1475fn generate_response(
1476    request: &Request,
1477    document_root: &Path,
1478) -> Result<Response, ServerError> {
1479    // Fallback entry point used only by tests: canonicalize lazily.
1480    let canonical = fs::canonicalize(document_root)
1481        .unwrap_or_else(|_| document_root.to_path_buf());
1482    generate_response_with_cache(
1483        request,
1484        document_root,
1485        &canonical,
1486        None,
1487        DEFAULT_MAX_BUFFERED_BODY_BYTES,
1488    )
1489}
1490
1491fn generate_response_with_cache(
1492    request: &Request,
1493    document_root: &Path,
1494    canonical_root: &Path,
1495    cache_ttl_secs: Option<u64>,
1496    max_buffered_body_bytes: u64,
1497) -> Result<Response, ServerError> {
1498    let mut path = PathBuf::from(document_root);
1499    let request_path = request.path().trim_start_matches('/');
1500
1501    if request_path.is_empty() {
1502        // If the request is for the root, append "index.html"
1503        path.push("index.html");
1504    } else {
1505        for component in request_path.split('/') {
1506            if component == ".." {
1507                let _ = path.pop();
1508            } else {
1509                path.push(component);
1510            }
1511        }
1512    }
1513
1514    let within_root = fs::canonicalize(&path)
1515        .map(|candidate| candidate.starts_with(canonical_root))
1516        .unwrap_or_else(|_| path.starts_with(document_root));
1517    if !within_root {
1518        return Err(ServerError::forbidden("Access denied"));
1519    }
1520
1521    if path.is_file() {
1522        serve_file_response(
1523            request,
1524            &path,
1525            cache_ttl_secs,
1526            max_buffered_body_bytes,
1527        )
1528    } else if path.is_dir() {
1529        // If it's a directory, try to serve index.html from that directory
1530        path.push("index.html");
1531        if path.is_file() {
1532            serve_file_response(
1533                request,
1534                &path,
1535                cache_ttl_secs,
1536                max_buffered_body_bytes,
1537            )
1538        } else {
1539            generate_404_response(document_root)
1540        }
1541    } else {
1542        generate_404_response(document_root)
1543    }
1544}
1545
1546fn serve_file_response(
1547    request: &Request,
1548    path: &Path,
1549    cache_ttl_secs: Option<u64>,
1550    max_buffered_body_bytes: u64,
1551) -> Result<Response, ServerError> {
1552    let mut serving_path = path.to_path_buf();
1553    let mut content_encoding: Option<&'static str> = None;
1554    if let Some(encoding) = request.header("accept-encoding") {
1555        if encoding.contains("br") {
1556            let candidate =
1557                PathBuf::from(format!("{}.br", path.display()));
1558            if candidate.is_file() {
1559                serving_path = candidate;
1560                content_encoding = Some("br");
1561            }
1562        }
1563        if content_encoding.is_none()
1564            && (encoding.contains("zstd") || encoding.contains("zst"))
1565        {
1566            let candidate =
1567                PathBuf::from(format!("{}.zst", path.display()));
1568            if candidate.is_file() {
1569                serving_path = candidate;
1570                content_encoding = Some("zstd");
1571            }
1572        }
1573        if content_encoding.is_none() && encoding.contains("gzip") {
1574            let candidate =
1575                PathBuf::from(format!("{}.gz", path.display()));
1576            if candidate.is_file() {
1577                serving_path = candidate;
1578                content_encoding = Some("gzip");
1579            }
1580        }
1581    }
1582
1583    // Pre-flight metadata check: refuse to buffer files larger than the
1584    // operator's configured cap. This prevents a single client from
1585    // driving server RSS to file_size; future streaming work will lift
1586    // the cap by writing directly to the wire instead of constructing
1587    // a Vec<u8>.
1588    let serving_metadata =
1589        fs::metadata(&serving_path).map_err(ServerError::from)?;
1590    if serving_metadata.len() > max_buffered_body_bytes {
1591        return Err(ServerError::Custom(format!(
1592            "file exceeds in-memory serve cap ({} > {} bytes); \
1593             override via ServerBuilder::max_buffered_body_bytes or \
1594             wait for v0.1 streaming",
1595            serving_metadata.len(),
1596            max_buffered_body_bytes
1597        )));
1598    }
1599    let contents = fs::read(&serving_path)?;
1600    let metadata = fs::metadata(path)?;
1601    let etag = compute_etag(&metadata);
1602    if request
1603        .header("if-none-match")
1604        .is_some_and(|candidate| candidate == &*etag)
1605    {
1606        let mut response =
1607            Response::new(304, "NOT MODIFIED", Vec::new());
1608        response.add_header("ETag", &etag);
1609        return Ok(response);
1610    }
1611
1612    let content_type = get_content_type(path);
1613    let mut response = if let Some((start, end)) =
1614        parse_range_header(request.header("range"), contents.len())
1615    {
1616        let body = contents[start..=end].to_vec();
1617        let mut partial = Response::new(206, "PARTIAL CONTENT", body);
1618        partial.add_header(
1619            "Content-Range",
1620            &format!("bytes {}-{}/{}", start, end, contents.len()),
1621        );
1622        partial
1623    } else {
1624        Response::new(200, "OK", contents)
1625    };
1626
1627    response.add_header("Content-Type", content_type);
1628    response.add_header("ETag", &etag);
1629    response.add_header("Accept-Ranges", "bytes");
1630    if let Some(encoding) = content_encoding {
1631        response.add_header("Content-Encoding", encoding);
1632        response.add_header("Vary", "Accept-Encoding");
1633    }
1634    if let Some(ttl) = cache_ttl_secs {
1635        response.add_header(
1636            "Cache-Control",
1637            &format!("public, max-age={ttl}"),
1638        );
1639    }
1640    Ok(response)
1641}
1642
1643fn etag_cache() -> &'static EtagCache {
1644    ETAG_CACHE.get_or_init(|| RwLock::new(HashMap::new()))
1645}
1646
1647fn compute_etag(metadata: &fs::Metadata) -> Arc<str> {
1648    let modified = metadata
1649        .modified()
1650        .ok()
1651        .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
1652        .map_or(0_u64, |duration| duration.as_secs());
1653    let len = metadata.len();
1654    let key = (len, modified);
1655
1656    let cache = etag_cache();
1657    if let Ok(read) = cache.read()
1658        && let Some(etag) = read.get(&key)
1659    {
1660        return Arc::clone(etag);
1661    }
1662
1663    // Miss: format the etag and insert. The Arc<str> is shared between
1664    // the cache and the caller, so subsequent hits return a refcount
1665    // bump instead of a fresh String allocation.
1666    let etag: Arc<str> =
1667        Arc::from(format!("W/\"{:x}-{:x}\"", len, modified));
1668
1669    if let Ok(mut write) = cache.write() {
1670        if write.len() >= ETAG_CACHE_MAX {
1671            // Crude eviction: drop the first quarter of entries when we
1672            // exceed the cap. Avoids unbounded growth without depending
1673            // on an LRU crate. Workloads with high path churn that fill
1674            // the cache get a periodic pause; typical static-content
1675            // serving stays inside the cap and never evicts.
1676            let drop_count = ETAG_CACHE_MAX / 4;
1677            let to_remove: Vec<_> =
1678                write.keys().take(drop_count).copied().collect();
1679            for k in to_remove {
1680                let _ = write.remove(&k);
1681            }
1682        }
1683        let _ = write.insert(key, Arc::clone(&etag));
1684    }
1685
1686    etag
1687}
1688
1689fn parse_range_header(
1690    header: Option<&str>,
1691    total_len: usize,
1692) -> Option<(usize, usize)> {
1693    let header = header?;
1694    let value = header.strip_prefix("bytes=")?;
1695    let (start_str, end_str) = value.split_once('-')?;
1696    if start_str.is_empty() && end_str.is_empty() {
1697        return None;
1698    }
1699    if start_str.is_empty() {
1700        let suffix_len = end_str.parse::<usize>().ok()?;
1701        if suffix_len == 0 || suffix_len > total_len {
1702            return None;
1703        }
1704        return Some((total_len - suffix_len, total_len - 1));
1705    }
1706    let start = start_str.parse::<usize>().ok()?;
1707    let end = if end_str.is_empty() {
1708        total_len.checked_sub(1)?
1709    } else {
1710        end_str.parse::<usize>().ok()?
1711    };
1712    if start > end || end >= total_len {
1713        return None;
1714    }
1715    Some((start, end))
1716}
1717
1718/// Generates a 404 Not Found response.
1719///
1720/// # Arguments
1721///
1722/// * `document_root` - A `Path` representing the server's document root.
1723///
1724/// # Returns
1725///
1726/// A `Result` containing the `Response` or a `ServerError`.
1727fn generate_404_response(
1728    document_root: &Path,
1729) -> Result<Response, ServerError> {
1730    let not_found_path = document_root.join("404/index.html");
1731    let contents = if not_found_path.is_file() {
1732        fs::read(not_found_path)?
1733    } else {
1734        b"404 Not Found".to_vec()
1735    };
1736    let mut response = Response::new(404, "NOT FOUND", contents);
1737    response.add_header("Content-Type", "text/html");
1738    Ok(response)
1739}
1740
1741/// Generates an HTTP HEAD response based on the requested file.
1742///
1743/// HEAD responses are identical to GET responses but without the message body.
1744/// The response headers, including Content-Length, must be identical to what
1745/// would be sent for a GET request to the same resource.
1746///
1747/// # Arguments
1748///
1749/// * `request` - A `Request` instance representing the client's request.
1750/// * `document_root` - A `Path` representing the server's document root.
1751///
1752/// # Returns
1753///
1754/// A `Result` containing the `Response` or a `ServerError`.
1755fn generate_head_response(
1756    request: &Request,
1757    document_root: &Path,
1758) -> Result<Response, ServerError> {
1759    // Generate the full response as if it were a GET request
1760    let full_response = generate_response(request, document_root)?;
1761
1762    // Create a new response with the same status and headers but empty body
1763    let mut head_response = Response::new(
1764        full_response.status_code,
1765        &full_response.status_text,
1766        Vec::new(), // Empty body for HEAD response
1767    );
1768
1769    // Copy all headers from the full response
1770    for (name, value) in &full_response.headers {
1771        head_response.add_header(name, value);
1772    }
1773
1774    // Add Content-Length header to match what would be sent in GET response
1775    let content_length = full_response.body.len().to_string();
1776    head_response.add_header("Content-Length", &content_length);
1777
1778    Ok(head_response)
1779}
1780
1781/// Generates an HTTP OPTIONS response indicating supported methods.
1782///
1783/// The OPTIONS method is used to describe the communication options for the target resource.
1784/// This implementation returns the allowed HTTP methods for any requested resource.
1785///
1786/// # Arguments
1787///
1788/// * `request` - A `Request` instance representing the client's request.
1789///
1790/// # Returns
1791///
1792/// A `Response` instance with allowed methods.
1793fn generate_options_response(
1794    _request: &Request,
1795) -> Result<Response, ServerError> {
1796    let mut response = Response::new(200, "OK", Vec::new());
1797    response.add_header("Allow", "GET, HEAD, OPTIONS");
1798    response.add_header("Content-Length", "0");
1799    Ok(response)
1800}
1801
1802/// Generates a 405 Method Not Allowed response.
1803///
1804/// This response is sent when the client uses an HTTP method that is not
1805/// supported by the server for the requested resource.
1806///
1807/// # Returns
1808///
1809/// A `Response` instance indicating the method is not allowed.
1810fn generate_method_not_allowed_response() -> Response {
1811    let mut response = Response::new(
1812        405,
1813        "METHOD NOT ALLOWED",
1814        b"Method Not Allowed".to_vec(),
1815    );
1816    response.add_header("Allow", "GET, HEAD, OPTIONS");
1817    response.add_header("Content-Type", "text/plain");
1818    response.add_header("Content-Length", "18");
1819    response
1820}
1821
1822fn response_from_error(
1823    error: &ServerError,
1824    document_root: &Path,
1825) -> Response {
1826    match error {
1827        ServerError::InvalidRequest(message) => {
1828            let mut response = Response::new(
1829                400,
1830                "BAD REQUEST",
1831                message.as_bytes().to_vec(),
1832            );
1833            response.add_header("Content-Type", "text/plain");
1834            response
1835        }
1836        ServerError::Forbidden(message) => {
1837            let mut response = Response::new(
1838                403,
1839                "FORBIDDEN",
1840                message.as_bytes().to_vec(),
1841            );
1842            response.add_header("Content-Type", "text/plain");
1843            response
1844        }
1845        ServerError::NotFound(_) => {
1846            generate_404_response(document_root).unwrap_or_else(|_| {
1847                let mut response = Response::new(
1848                    404,
1849                    "NOT FOUND",
1850                    b"404 Not Found".to_vec(),
1851                );
1852                response.add_header("Content-Type", "text/plain");
1853                response
1854            })
1855        }
1856        ServerError::Io(_)
1857        | ServerError::Custom(_)
1858        | ServerError::TaskFailed(_) => {
1859            let mut response = Response::new(
1860                500,
1861                "INTERNAL SERVER ERROR",
1862                b"Internal Server Error".to_vec(),
1863            );
1864            response.add_header("Content-Type", "text/plain");
1865            response
1866        }
1867    }
1868}
1869
1870fn apply_response_policies(
1871    mut response: Response,
1872    server: &Server,
1873    request: &Request,
1874) -> Response {
1875    if let Some(headers) = server.custom_headers.as_ref() {
1876        for (name, value) in headers {
1877            response.add_header(name, value);
1878        }
1879    }
1880
1881    if server.cors_enabled.unwrap_or(false) {
1882        let allow_origin = server
1883            .cors_origins
1884            .as_ref()
1885            .and_then(|origins| origins.first())
1886            .map(String::as_str)
1887            .unwrap_or("*");
1888        response
1889            .add_header("Access-Control-Allow-Origin", allow_origin);
1890        response.add_header(
1891            "Access-Control-Allow-Methods",
1892            "GET, HEAD, OPTIONS",
1893        );
1894        response.add_header("Access-Control-Allow-Headers", "*");
1895
1896        if request.method().eq_ignore_ascii_case("OPTIONS") {
1897            response.add_header("Access-Control-Max-Age", "600");
1898        }
1899    }
1900
1901    if let Some(ttl) = server.static_cache_ttl_secs {
1902        let has_cache_control =
1903            response.headers.iter().any(|(name, _)| {
1904                name.eq_ignore_ascii_case("cache-control")
1905            });
1906        if !has_cache_control {
1907            if is_probably_immutable_asset_path(request.path()) {
1908                response.add_header(
1909                    "Cache-Control",
1910                    "public, max-age=31536000, immutable",
1911                );
1912            } else {
1913                response.add_header(
1914                    "Cache-Control",
1915                    &format!("public, max-age={ttl}"),
1916                );
1917            }
1918        }
1919    }
1920
1921    response
1922}
1923
1924fn is_probably_immutable_asset_path(path: &str) -> bool {
1925    let file = path.rsplit('/').next().unwrap_or(path);
1926    let Some((stem, _ext)) = file.rsplit_once('.') else {
1927        return false;
1928    };
1929    let Some(hash) = stem.rsplit('-').next() else {
1930        return false;
1931    };
1932    hash.len() >= 8 && hash.chars().all(|c| c.is_ascii_hexdigit())
1933}
1934
1935/// Determines the content type based on the file extension.
1936///
1937/// # Arguments
1938///
1939/// * `path` - A `Path` representing the file path.
1940///
1941/// # Returns
1942///
1943/// A string slice representing the content type.
1944fn get_content_type(path: &Path) -> &'static str {
1945    match path.extension().and_then(std::ffi::OsStr::to_str) {
1946        // Text formats
1947        Some("html") | Some("htm") => "text/html",
1948        Some("css") => "text/css",
1949        Some("js") | Some("mjs") => "application/javascript",
1950        Some("ts") => "application/typescript",
1951        Some("json") => "application/json",
1952        Some("xml") => "application/xml",
1953        Some("txt") => "text/plain",
1954        Some("md") | Some("markdown") => "text/markdown",
1955        Some("yaml") | Some("yml") => "application/x-yaml",
1956        Some("toml") => "application/toml",
1957
1958        // Traditional image formats
1959        Some("png") => "image/png",
1960        Some("jpg") | Some("jpeg") => "image/jpeg",
1961        Some("gif") => "image/gif",
1962        Some("svg") => "image/svg+xml",
1963        Some("ico") => "image/x-icon",
1964
1965        // Modern image formats
1966        Some("webp") => "image/webp",
1967        Some("avif") => "image/avif",
1968        Some("heic") | Some("heif") => "image/heic",
1969        Some("jxl") => "image/jxl",
1970        Some("bmp") => "image/bmp",
1971        Some("tiff") | Some("tif") => "image/tiff",
1972
1973        // Web Assembly
1974        Some("wasm") => "application/wasm",
1975
1976        // Font formats
1977        Some("woff") => "font/woff",
1978        Some("woff2") => "font/woff2",
1979        Some("ttf") => "font/ttf",
1980        Some("otf") => "font/otf",
1981        Some("eot") => "application/vnd.ms-fontobject",
1982
1983        // Audio formats
1984        Some("mp3") => "audio/mpeg",
1985        Some("wav") => "audio/wav",
1986        Some("ogg") => "audio/ogg",
1987        Some("opus") => "audio/opus",
1988        Some("flac") => "audio/flac",
1989        Some("m4a") => "audio/mp4",
1990        Some("aac") => "audio/aac",
1991
1992        // Video formats
1993        Some("mp4") => "video/mp4",
1994        Some("webm") => "video/webm",
1995        Some("av1") => "video/av1",
1996        Some("avi") => "video/x-msvideo",
1997        Some("mov") => "video/quicktime",
1998
1999        // Document formats
2000        Some("pdf") => "application/pdf",
2001        Some("zip") => "application/zip",
2002        Some("tar") => "application/x-tar",
2003        Some("gz") => "application/gzip",
2004
2005        // Development formats
2006        Some("map") => "application/json", // Source maps
2007        Some("webmanifest") => "application/manifest+json",
2008
2009        // Default fallback
2010        _ => "application/octet-stream",
2011    }
2012}
2013
2014#[cfg(test)]
2015mod tests {
2016    use super::*;
2017    use std::fs::File;
2018    use std::io;
2019    use std::io::Read;
2020    use std::io::Write;
2021    use std::net::{TcpListener, TcpStream};
2022    use tempfile::TempDir;
2023
2024    fn setup_test_directory() -> TempDir {
2025        let temp_dir = TempDir::new().unwrap();
2026        let root_path = temp_dir.path();
2027
2028        // Create index.html
2029        let mut index_file =
2030            File::create(root_path.join("index.html")).unwrap();
2031        index_file
2032            .write_all(b"<html><body>Hello, World!</body></html>")
2033            .unwrap();
2034
2035        // Create 404/index.html
2036        fs::create_dir(root_path.join("404")).unwrap();
2037        let mut not_found_file =
2038            File::create(root_path.join("404/index.html")).unwrap();
2039        not_found_file
2040            .write_all(b"<html><body>404 Not Found</body></html>")
2041            .unwrap();
2042
2043        // Create a subdirectory with its own index.html
2044        fs::create_dir(root_path.join("subdir")).unwrap();
2045        let mut subdir_index_file =
2046            File::create(root_path.join("subdir/index.html")).unwrap();
2047        subdir_index_file
2048            .write_all(b"<html><body>Subdirectory Index</body></html>")
2049            .unwrap();
2050
2051        temp_dir
2052    }
2053
2054    fn roundtrip_handle_connection(
2055        server: &Server,
2056        request: &[u8],
2057    ) -> String {
2058        let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
2059        let addr = listener.local_addr().expect("addr");
2060        let server_clone = server.clone();
2061        let handle = thread::spawn(move || {
2062            let (stream, _) = listener.accept().expect("accept");
2063            handle_connection(stream, &server_clone).expect("handle");
2064        });
2065
2066        let mut client = TcpStream::connect(addr).expect("connect");
2067        // Inject `Connection: close` if the test request didn't already
2068        // include one. With HTTP/1.1 keep-alive on by default the
2069        // server otherwise sits in the idle loop until KEEPALIVE_IDLE_TIMEOUT
2070        // fires, dragging single-shot integration tests into multi-second
2071        // territory. Tests that explicitly want keep-alive can include
2072        // their own Connection header.
2073        let request_text = std::str::from_utf8(request).unwrap_or("");
2074        if request_text.to_ascii_lowercase().contains("connection:") {
2075            client.write_all(request).expect("write");
2076        } else {
2077            // Splice "Connection: close\r\n" before the trailing CRLF
2078            // that ends the headers section.
2079            let with_close =
2080                if let Some(idx) = request_text.rfind("\r\n\r\n") {
2081                    let (head, tail) = request_text.split_at(idx);
2082                    format!("{head}\r\nConnection: close{tail}")
2083                } else {
2084                    request_text.to_string()
2085                };
2086            client.write_all(with_close.as_bytes()).expect("write");
2087        }
2088        let mut response = String::new();
2089        let _ = client.read_to_string(&mut response).expect("read");
2090        handle.join().expect("join");
2091        response
2092    }
2093
2094    fn connected_pair() -> (TcpStream, TcpStream) {
2095        let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
2096        let addr = listener.local_addr().expect("addr");
2097        let client = TcpStream::connect(addr).expect("connect");
2098        let (server, _) = listener.accept().expect("accept");
2099        (server, client)
2100    }
2101
2102    #[test]
2103    fn test_server_creation() {
2104        let server = Server::new("127.0.0.1:8080", "/var/www");
2105        assert_eq!(server.address, "127.0.0.1:8080");
2106        assert_eq!(server.document_root, PathBuf::from("/var/www"));
2107    }
2108
2109    #[test]
2110    fn test_get_content_type() {
2111        assert_eq!(
2112            get_content_type(Path::new("test.html")),
2113            "text/html"
2114        );
2115        assert_eq!(
2116            get_content_type(Path::new("page.htm")),
2117            "text/html"
2118        );
2119        assert_eq!(
2120            get_content_type(Path::new("style.css")),
2121            "text/css"
2122        );
2123        assert_eq!(
2124            get_content_type(Path::new("script.js")),
2125            "application/javascript"
2126        );
2127        assert_eq!(
2128            get_content_type(Path::new("data.json")),
2129            "application/json"
2130        );
2131        assert_eq!(
2132            get_content_type(Path::new("image.png")),
2133            "image/png"
2134        );
2135        assert_eq!(
2136            get_content_type(Path::new("photo.jpg")),
2137            "image/jpeg"
2138        );
2139        assert_eq!(
2140            get_content_type(Path::new("animation.gif")),
2141            "image/gif"
2142        );
2143        assert_eq!(
2144            get_content_type(Path::new("icon.svg")),
2145            "image/svg+xml"
2146        );
2147        assert_eq!(
2148            get_content_type(Path::new("unknown.xyz")),
2149            "application/octet-stream"
2150        );
2151    }
2152
2153    #[test]
2154    fn test_modern_content_types() {
2155        // Test modern image formats
2156        assert_eq!(
2157            get_content_type(Path::new("image.webp")),
2158            "image/webp"
2159        );
2160        assert_eq!(
2161            get_content_type(Path::new("image.avif")),
2162            "image/avif"
2163        );
2164        assert_eq!(
2165            get_content_type(Path::new("image.heic")),
2166            "image/heic"
2167        );
2168        assert_eq!(
2169            get_content_type(Path::new("image.heif")),
2170            "image/heic"
2171        );
2172        assert_eq!(
2173            get_content_type(Path::new("image.jxl")),
2174            "image/jxl"
2175        );
2176
2177        // Test Web Assembly
2178        assert_eq!(
2179            get_content_type(Path::new("module.wasm")),
2180            "application/wasm"
2181        );
2182
2183        // Test modern text formats
2184        assert_eq!(
2185            get_content_type(Path::new("script.ts")),
2186            "application/typescript"
2187        );
2188        assert_eq!(
2189            get_content_type(Path::new("module.mjs")),
2190            "application/javascript"
2191        );
2192        assert_eq!(
2193            get_content_type(Path::new("README.md")),
2194            "text/markdown"
2195        );
2196        assert_eq!(
2197            get_content_type(Path::new("config.yaml")),
2198            "application/x-yaml"
2199        );
2200        assert_eq!(
2201            get_content_type(Path::new("config.yml")),
2202            "application/x-yaml"
2203        );
2204        assert_eq!(
2205            get_content_type(Path::new("Cargo.toml")),
2206            "application/toml"
2207        );
2208
2209        // Test modern audio formats
2210        assert_eq!(
2211            get_content_type(Path::new("audio.opus")),
2212            "audio/opus"
2213        );
2214        assert_eq!(
2215            get_content_type(Path::new("audio.flac")),
2216            "audio/flac"
2217        );
2218
2219        // Test modern video formats
2220        assert_eq!(
2221            get_content_type(Path::new("video.av1")),
2222            "video/av1"
2223        );
2224
2225        // Test development formats
2226        assert_eq!(
2227            get_content_type(Path::new("script.js.map")),
2228            "application/json"
2229        );
2230        assert_eq!(
2231            get_content_type(Path::new("manifest.webmanifest")),
2232            "application/manifest+json"
2233        );
2234    }
2235
2236    #[test]
2237    fn test_generate_response() {
2238        let temp_dir = setup_test_directory();
2239        let document_root = temp_dir.path();
2240
2241        // Test root request (should serve index.html)
2242        let root_request = Request {
2243            method: "GET".to_string(),
2244            path: "/".to_string(),
2245            version: "HTTP/1.1".to_string(),
2246            headers: Vec::new(),
2247        };
2248
2249        let root_response =
2250            generate_response(&root_request, document_root).unwrap();
2251        assert_eq!(root_response.status_code, 200);
2252        assert_eq!(root_response.status_text, "OK");
2253        assert!(
2254            root_response.body.starts_with(
2255                b"<html><body>Hello, World!</body></html>"
2256            )
2257        );
2258
2259        // Test specific file request
2260        let file_request = Request {
2261            method: "GET".to_string(),
2262            path: "/index.html".to_string(),
2263            version: "HTTP/1.1".to_string(),
2264            headers: Vec::new(),
2265        };
2266
2267        let file_response =
2268            generate_response(&file_request, document_root).unwrap();
2269        assert_eq!(file_response.status_code, 200);
2270        assert_eq!(file_response.status_text, "OK");
2271        assert!(
2272            file_response.body.starts_with(
2273                b"<html><body>Hello, World!</body></html>"
2274            )
2275        );
2276
2277        // Test subdirectory index request
2278        let subdir_request = Request {
2279            method: "GET".to_string(),
2280            path: "/subdir/".to_string(),
2281            version: "HTTP/1.1".to_string(),
2282            headers: Vec::new(),
2283        };
2284
2285        let subdir_response =
2286            generate_response(&subdir_request, document_root).unwrap();
2287        assert_eq!(subdir_response.status_code, 200);
2288        assert_eq!(subdir_response.status_text, "OK");
2289        assert!(subdir_response.body.starts_with(
2290            b"<html><body>Subdirectory Index</body></html>"
2291        ));
2292
2293        // Test non-existent file request
2294        let not_found_request = Request {
2295            method: "GET".to_string(),
2296            path: "/nonexistent.html".to_string(),
2297            version: "HTTP/1.1".to_string(),
2298            headers: Vec::new(),
2299        };
2300
2301        let not_found_response =
2302            generate_response(&not_found_request, document_root)
2303                .unwrap();
2304        assert_eq!(not_found_response.status_code, 404);
2305        assert_eq!(not_found_response.status_text, "NOT FOUND");
2306        assert!(
2307            not_found_response.body.starts_with(
2308                b"<html><body>404 Not Found</body></html>"
2309            )
2310        );
2311
2312        // Test directory traversal attempt
2313        let traversal_request = Request {
2314            method: "GET".to_string(),
2315            path: "/../outside.html".to_string(),
2316            version: "HTTP/1.1".to_string(),
2317            headers: Vec::new(),
2318        };
2319
2320        let traversal_response =
2321            generate_response(&traversal_request, document_root);
2322        assert!(matches!(
2323            traversal_response,
2324            Err(ServerError::Forbidden(_))
2325        ));
2326    }
2327
2328    #[test]
2329    fn test_server_builder() {
2330        // Test basic ServerBuilder usage
2331        let server = Server::builder()
2332            .address("127.0.0.1:8080")
2333            .document_root("/var/www")
2334            .enable_cors()
2335            .custom_header("X-Custom", "test-value")
2336            .request_timeout(Duration::from_secs(30))
2337            .build()
2338            .unwrap();
2339
2340        assert_eq!(server.address, "127.0.0.1:8080");
2341        assert_eq!(server.document_root, PathBuf::from("/var/www"));
2342        assert_eq!(server.cors_enabled, Some(true));
2343        assert_eq!(
2344            server.request_timeout,
2345            Some(Duration::from_secs(30))
2346        );
2347
2348        // Check custom headers
2349        let headers = server.custom_headers.unwrap();
2350        assert_eq!(
2351            headers.get("X-Custom"),
2352            Some(&"test-value".to_string())
2353        );
2354
2355        // Test builder error handling
2356        let result = ServerBuilder::new().build();
2357        assert!(result.is_err());
2358
2359        // Test CORS origins setting
2360        let server_with_origins = Server::builder()
2361            .address("127.0.0.1:9000")
2362            .document_root("/tmp")
2363            .cors_origins(vec!["https://example.com".to_string()])
2364            .build()
2365            .unwrap();
2366
2367        assert_eq!(server_with_origins.cors_enabled, Some(true));
2368        assert_eq!(
2369            server_with_origins.cors_origins,
2370            Some(vec!["https://example.com".to_string()])
2371        );
2372    }
2373
2374    #[test]
2375    fn test_graceful_shutdown() {
2376        // Test ShutdownSignal creation and default behavior
2377        let shutdown = ShutdownSignal::new(Duration::from_secs(5));
2378
2379        // Initially no shutdown should be requested
2380        assert!(!shutdown.is_shutdown_requested());
2381        assert_eq!(shutdown.active_connection_count(), 0);
2382
2383        // Test connection tracking
2384        shutdown.connection_started();
2385        assert_eq!(shutdown.active_connection_count(), 1);
2386
2387        shutdown.connection_started();
2388        assert_eq!(shutdown.active_connection_count(), 2);
2389
2390        shutdown.connection_finished();
2391        assert_eq!(shutdown.active_connection_count(), 1);
2392
2393        shutdown.connection_finished();
2394        assert_eq!(shutdown.active_connection_count(), 0);
2395
2396        // Test shutdown signal
2397        shutdown.shutdown();
2398        assert!(shutdown.is_shutdown_requested());
2399
2400        // Test immediate shutdown when no active connections
2401        let graceful = shutdown.wait_for_shutdown();
2402        assert!(graceful);
2403    }
2404
2405    #[test]
2406    fn test_shutdown_signal_timeout() {
2407        let shutdown = ShutdownSignal::new(Duration::from_millis(100));
2408
2409        // Start a connection and request shutdown
2410        shutdown.connection_started();
2411        shutdown.shutdown();
2412
2413        // Should timeout since connection never finishes
2414        let graceful = shutdown.wait_for_shutdown();
2415        assert!(!graceful); // Should be false due to timeout
2416    }
2417
2418    #[test]
2419    fn test_thread_pool() {
2420        use std::sync::Arc;
2421        use std::sync::atomic::AtomicUsize;
2422        use std::sync::mpsc;
2423
2424        let pool = ThreadPool::new(4);
2425        let counter = Arc::new(AtomicUsize::new(0));
2426        let (tx, rx) = mpsc::channel();
2427
2428        // Execute 10 jobs
2429        for _ in 0..10 {
2430            let counter_clone = Arc::clone(&counter);
2431            let tx_clone = tx.clone();
2432
2433            pool.execute(move || {
2434                let _ = counter_clone.fetch_add(1, Ordering::SeqCst);
2435                tx_clone.send(()).unwrap();
2436            });
2437        }
2438
2439        // Wait for all jobs to complete
2440        for _ in 0..10 {
2441            rx.recv().unwrap();
2442        }
2443
2444        assert_eq!(counter.load(Ordering::SeqCst), 10);
2445    }
2446
2447    #[test]
2448    fn test_connection_pool() {
2449        let pool = ConnectionPool::new(2);
2450        assert_eq!(pool.active_count(), 0);
2451
2452        // Acquire first connection
2453        let guard1 = pool.acquire().unwrap();
2454        assert_eq!(pool.active_count(), 1);
2455
2456        // Acquire second connection
2457        let guard2 = pool.acquire().unwrap();
2458        assert_eq!(pool.active_count(), 2);
2459
2460        // Try to acquire third connection (should fail)
2461        let result = pool.acquire();
2462        assert!(result.is_err());
2463        assert_eq!(pool.active_count(), 2);
2464
2465        // Drop first connection
2466        drop(guard1);
2467        assert_eq!(pool.active_count(), 1);
2468
2469        // Should be able to acquire again
2470        let _guard3 = pool.acquire().unwrap();
2471        assert_eq!(pool.active_count(), 2);
2472
2473        // Drop all connections
2474        drop(guard2);
2475        drop(_guard3);
2476        assert_eq!(pool.active_count(), 0);
2477    }
2478
2479    #[test]
2480    fn test_thread_pool_concurrent_execution() {
2481        use std::sync::Arc;
2482        use std::sync::atomic::AtomicUsize;
2483        use std::sync::mpsc;
2484        use std::time::Instant;
2485
2486        let pool = ThreadPool::new(4);
2487        let counter = Arc::new(AtomicUsize::new(0));
2488        let (tx, rx) = mpsc::channel();
2489
2490        let start_time = Instant::now();
2491
2492        // Execute 100 jobs that should be processed concurrently
2493        for i in 0..100 {
2494            let counter_clone = Arc::clone(&counter);
2495            let tx_clone = tx.clone();
2496
2497            pool.execute(move || {
2498                // Simulate some work
2499                thread::sleep(Duration::from_millis(10));
2500                let _ = counter_clone.fetch_add(1, Ordering::SeqCst);
2501                tx_clone.send(i).unwrap();
2502            });
2503        }
2504
2505        // Wait for all jobs to complete
2506        for _ in 0..100 {
2507            let _ = rx.recv().unwrap();
2508        }
2509
2510        let elapsed = start_time.elapsed();
2511        assert_eq!(counter.load(Ordering::SeqCst), 100);
2512
2513        // With 4 threads, 100 jobs of 10ms each should complete much faster than 1000ms
2514        assert!(
2515            elapsed.as_millis() < 800,
2516            "Thread pool should provide concurrency benefits"
2517        );
2518    }
2519
2520    #[test]
2521    fn test_connection_pool_backpressure() {
2522        let pool = ConnectionPool::new(2);
2523
2524        // Acquire maximum connections
2525        let _guard1 = pool.acquire().unwrap();
2526        let _guard2 = pool.acquire().unwrap();
2527        assert_eq!(pool.active_count(), 2);
2528
2529        // Additional connection should be rejected
2530        let result = pool.acquire();
2531        assert!(result.is_err());
2532        assert_eq!(
2533            result.unwrap_err().kind(),
2534            io::ErrorKind::WouldBlock
2535        );
2536    }
2537
2538    #[test]
2539    fn test_service_unavailable_response() {
2540        // Create a test TCP connection
2541        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
2542        let addr = listener.local_addr().unwrap();
2543
2544        let _ = thread::spawn(move || {
2545            let (stream, _) = listener.accept().unwrap();
2546            send_service_unavailable(stream).unwrap();
2547        });
2548
2549        let mut client_stream = TcpStream::connect(addr).unwrap();
2550        let mut response = String::new();
2551        let _ = client_stream.read_to_string(&mut response).unwrap();
2552
2553        assert!(response.contains("503"));
2554        assert!(response.contains("SERVICE UNAVAILABLE"));
2555        assert!(response.contains("Service temporarily unavailable"));
2556        assert!(response.contains("Retry-After: 1"));
2557    }
2558
2559    #[test]
2560    fn test_service_unavailable_send_failure_is_mapped() {
2561        use std::net::Shutdown;
2562
2563        let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
2564        let addr = listener.local_addr().expect("addr");
2565
2566        let t = thread::spawn(move || {
2567            let (stream, _) = listener.accept().expect("accept");
2568            stream.shutdown(Shutdown::Write).expect("shutdown");
2569            let err =
2570                send_service_unavailable(stream).expect_err("err");
2571            assert!(
2572                err.to_string().contains("Failed to send response")
2573            );
2574        });
2575
2576        let _client = TcpStream::connect(addr).expect("connect");
2577        t.join().expect("join");
2578    }
2579
2580    #[test]
2581    fn test_response_from_error_variants() {
2582        let temp_dir = setup_test_directory();
2583        let root = temp_dir.path();
2584
2585        let bad = response_from_error(
2586            &ServerError::InvalidRequest("bad".to_string()),
2587            root,
2588        );
2589        assert_eq!(bad.status_code, 400);
2590
2591        let forbidden = response_from_error(
2592            &ServerError::Forbidden("no".to_string()),
2593            root,
2594        );
2595        assert_eq!(forbidden.status_code, 403);
2596
2597        let not_found = response_from_error(
2598            &ServerError::NotFound("missing".to_string()),
2599            root,
2600        );
2601        assert_eq!(not_found.status_code, 404);
2602
2603        let internal = response_from_error(
2604            &ServerError::TaskFailed("boom".to_string()),
2605            root,
2606        );
2607        assert_eq!(internal.status_code, 500);
2608    }
2609
2610    #[test]
2611    fn test_apply_response_policies_with_cors_and_headers() {
2612        let mut headers = HashMap::new();
2613        let _ = headers
2614            .insert("X-App".to_string(), "http-handle".to_string());
2615        let server = Server::builder()
2616            .address("127.0.0.1:0")
2617            .document_root(".")
2618            .enable_cors()
2619            .cors_origins(vec!["https://example.com".to_string()])
2620            .custom_headers(headers)
2621            .build()
2622            .expect("server builder");
2623
2624        let request = Request {
2625            method: "OPTIONS".to_string(),
2626            path: "/".to_string(),
2627            version: "HTTP/1.1".to_string(),
2628            headers: Vec::new(),
2629        };
2630        let response = apply_response_policies(
2631            Response::new(200, "OK", Vec::new()),
2632            &server,
2633            &request,
2634        );
2635
2636        let has_origin = response.headers.iter().any(|(k, v)| {
2637            k.eq_ignore_ascii_case("Access-Control-Allow-Origin")
2638                && v == "https://example.com"
2639        });
2640        let has_custom = response
2641            .headers
2642            .iter()
2643            .any(|(k, v)| k == "X-App" && v == "http-handle");
2644        let has_max_age = response.headers.iter().any(|(k, _)| {
2645            k.eq_ignore_ascii_case("Access-Control-Max-Age")
2646        });
2647
2648        assert!(has_origin);
2649        assert!(has_custom);
2650        assert!(has_max_age);
2651    }
2652
2653    #[test]
2654    fn test_thread_pool_debug_representation() {
2655        let pool = ThreadPool::new(1);
2656        let rendered = format!("{pool:?}");
2657        assert!(rendered.contains("ThreadPool"));
2658        assert!(rendered.contains("<Sender<Job>>"));
2659    }
2660
2661    #[test]
2662    fn test_server_getters_expose_builder_config() {
2663        let mut headers = HashMap::new();
2664        let _ =
2665            headers.insert("X-Test".to_string(), "value".to_string());
2666
2667        let server = Server::builder()
2668            .address("127.0.0.1:9001")
2669            .document_root("/tmp")
2670            .enable_cors()
2671            .cors_origins(vec!["https://example.com".to_string()])
2672            .custom_headers(headers)
2673            .request_timeout(Duration::from_secs(5))
2674            .connection_timeout(Duration::from_secs(7))
2675            .build()
2676            .expect("server");
2677
2678        assert_eq!(server.cors_enabled(), Some(true));
2679        assert_eq!(
2680            server.cors_origins(),
2681            &Some(vec!["https://example.com".to_string()])
2682        );
2683        assert_eq!(
2684            server.request_timeout(),
2685            Some(Duration::from_secs(5))
2686        );
2687        assert_eq!(
2688            server.connection_timeout(),
2689            Some(Duration::from_secs(7))
2690        );
2691        assert_eq!(server.address(), "127.0.0.1:9001");
2692        assert_eq!(server.document_root(), &PathBuf::from("/tmp"));
2693        assert_eq!(
2694            server
2695                .custom_headers()
2696                .as_ref()
2697                .and_then(|h| h.get("X-Test")),
2698            Some(&"value".to_string())
2699        );
2700    }
2701
2702    #[test]
2703    fn test_start_variants_return_bind_errors_for_in_use_address() {
2704        let occupied = TcpListener::bind("127.0.0.1:0").expect("bind");
2705        let addr = occupied.local_addr().expect("addr").to_string();
2706        let server = Server::new(&addr, ".");
2707
2708        assert!(server.start().is_err());
2709        assert!(
2710            server
2711                .start_with_graceful_shutdown(Duration::from_millis(1))
2712                .is_err()
2713        );
2714        assert!(server.start_with_thread_pool(1).is_err());
2715        assert!(server.start_with_pooling(1, 1).is_err());
2716    }
2717
2718    #[test]
2719    fn test_start_with_shutdown_signal_and_ready_reports_bound_address()
2720    {
2721        let root = setup_test_directory();
2722        let server = Server::builder()
2723            .address("127.0.0.1:0")
2724            .document_root(root.path().to_str().expect("path"))
2725            .build()
2726            .expect("server");
2727
2728        let (ready_tx, ready_rx) = mpsc::channel::<String>();
2729        let shutdown =
2730            Arc::new(ShutdownSignal::new(Duration::from_secs(2)));
2731        let shutdown_for_server = shutdown.clone();
2732        let server_for_thread = server.clone();
2733
2734        let handle = thread::spawn(move || {
2735            server_for_thread
2736                .start_with_shutdown_signal_and_ready(
2737                    shutdown_for_server,
2738                    move |addr| {
2739                        let _ = ready_tx.send(addr);
2740                    },
2741                )
2742                .expect("server run");
2743        });
2744
2745        let bound_addr = ready_rx
2746            .recv_timeout(Duration::from_secs(2))
2747            .expect("bound address");
2748        assert!(bound_addr.starts_with("127.0.0.1:"));
2749        assert_ne!(bound_addr, "127.0.0.1:0");
2750
2751        let mut stream =
2752            TcpStream::connect(&bound_addr).expect("connect");
2753        stream
2754            .write_all(
2755                b"GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n",
2756            )
2757            .expect("write");
2758        let mut response = String::new();
2759        let _ = stream.read_to_string(&mut response);
2760        assert!(response.starts_with("HTTP/1.1 200 OK"));
2761
2762        shutdown.shutdown();
2763        handle.join().expect("join");
2764    }
2765
2766    #[test]
2767    fn test_generate_response_falls_back_to_builtin_404_without_page() {
2768        let temp_dir = TempDir::new().expect("tmp");
2769        fs::write(temp_dir.path().join("index.html"), b"index")
2770            .expect("write");
2771        fs::create_dir(temp_dir.path().join("empty-dir")).expect("dir");
2772
2773        let request = Request {
2774            method: "GET".to_string(),
2775            path: "/empty-dir/".to_string(),
2776            version: "HTTP/1.1".to_string(),
2777            headers: Vec::new(),
2778        };
2779
2780        let response = generate_response(&request, temp_dir.path())
2781            .expect("response");
2782        assert_eq!(response.status_code, 404);
2783        assert_eq!(response.body, b"404 Not Found".to_vec());
2784    }
2785
2786    #[cfg(unix)]
2787    #[test]
2788    fn test_response_from_error_not_found_fallback_when_custom_404_unreadable()
2789     {
2790        use std::os::unix::fs::PermissionsExt;
2791
2792        let temp_dir = TempDir::new().expect("tmp");
2793        let custom_404_dir = temp_dir.path().join("404");
2794        fs::create_dir(&custom_404_dir).expect("create 404 dir");
2795        let custom_404 = custom_404_dir.join("index.html");
2796        fs::write(&custom_404, b"custom").expect("write 404");
2797        fs::set_permissions(
2798            &custom_404,
2799            fs::Permissions::from_mode(0o000),
2800        )
2801        .expect("chmod");
2802
2803        let response = response_from_error(
2804            &ServerError::NotFound("missing".to_string()),
2805            temp_dir.path(),
2806        );
2807
2808        assert_eq!(response.status_code, 404);
2809        assert_eq!(response.status_text, "NOT FOUND");
2810        assert_eq!(response.body, b"404 Not Found".to_vec());
2811    }
2812
2813    #[test]
2814    fn test_handle_connection_options_and_parse_error_paths() {
2815        let root = setup_test_directory();
2816        let root_str = root.path().to_str().expect("root path");
2817        let server = Server::builder()
2818            .address("127.0.0.1:0")
2819            .document_root(root_str)
2820            .build()
2821            .expect("server");
2822
2823        let options_response = roundtrip_handle_connection(
2824            &server,
2825            b"OPTIONS / HTTP/1.1\r\nHost: localhost\r\n\r\n",
2826        );
2827        assert!(options_response.starts_with("HTTP/1.1 200 OK"));
2828        assert!(options_response.contains("Allow: GET, HEAD, OPTIONS"));
2829
2830        let head_response = roundtrip_handle_connection(
2831            &server,
2832            b"HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n",
2833        );
2834        assert!(head_response.starts_with("HTTP/1.1 200 OK"));
2835        assert!(head_response.contains("Content-Length:"));
2836
2837        let method_not_allowed = roundtrip_handle_connection(
2838            &server,
2839            b"POST / HTTP/1.1\r\nHost: localhost\r\n\r\n",
2840        );
2841        assert!(
2842            method_not_allowed
2843                .starts_with("HTTP/1.1 405 METHOD NOT ALLOWED")
2844        );
2845
2846        let traversal_response = roundtrip_handle_connection(
2847            &server,
2848            b"GET /../outside HTTP/1.1\r\nHost: localhost\r\n\r\n",
2849        );
2850        assert!(
2851            traversal_response.starts_with("HTTP/1.1 403 FORBIDDEN")
2852        );
2853
2854        let bad_response =
2855            roundtrip_handle_connection(&server, b"BAD\r\n\r\n");
2856        assert!(bad_response.starts_with("HTTP/1.1 400 BAD REQUEST"));
2857    }
2858
2859    #[test]
2860    fn test_generate_response_supports_etag_and_range() {
2861        let temp_dir = setup_test_directory();
2862        let root = temp_dir.path();
2863
2864        let headers =
2865            vec![("range".to_string(), "bytes=0-4".to_string())];
2866        let range_request = Request {
2867            method: "GET".to_string(),
2868            path: "/index.html".to_string(),
2869            version: "HTTP/1.1".to_string(),
2870            headers,
2871        };
2872        let range_response =
2873            generate_response(&range_request, root).expect("range");
2874        assert_eq!(range_response.status_code, 206);
2875        assert!(range_response.body.starts_with(b"<html"));
2876        let etag = range_response
2877            .headers
2878            .iter()
2879            .find(|(name, _)| name.eq_ignore_ascii_case("etag"))
2880            .map(|(_, value)| value.clone())
2881            .expect("etag");
2882
2883        let headers = vec![("if-none-match".to_string(), etag)];
2884        let conditional_request = Request {
2885            method: "GET".to_string(),
2886            path: "/index.html".to_string(),
2887            version: "HTTP/1.1".to_string(),
2888            headers,
2889        };
2890        let conditional_response =
2891            generate_response(&conditional_request, root)
2892                .expect("conditional");
2893        assert_eq!(conditional_response.status_code, 304);
2894        assert!(conditional_response.body.is_empty());
2895    }
2896
2897    #[test]
2898    fn test_metrics_and_rate_limiting() {
2899        let root = setup_test_directory();
2900        let server = Server::builder()
2901            .address("127.0.0.1:0")
2902            .document_root(root.path().to_str().expect("path"))
2903            .rate_limit_per_minute(1)
2904            .build()
2905            .expect("server");
2906
2907        let _ = roundtrip_handle_connection(
2908            &server,
2909            b"GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n",
2910        );
2911        let limited = roundtrip_handle_connection(
2912            &server,
2913            b"GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n",
2914        );
2915        assert!(limited.starts_with("HTTP/1.1 429 TOO MANY REQUESTS"));
2916
2917        let metrics = roundtrip_handle_connection(
2918            &server,
2919            b"GET /metrics HTTP/1.1\r\nHost: localhost\r\n\r\n",
2920        );
2921        assert!(metrics.starts_with("HTTP/1.1 200 OK"));
2922        assert!(metrics.contains("http_handle_requests_total"));
2923    }
2924
2925    #[test]
2926    fn test_trigger_shutdown_from_slot_helper() {
2927        let shutdown =
2928            Arc::new(ShutdownSignal::new(Duration::from_secs(1)));
2929        let slot = Mutex::new(Some(shutdown.clone()));
2930        assert!(!shutdown.is_shutdown_requested());
2931        Server::trigger_shutdown_from_slot(&slot);
2932        assert!(shutdown.is_shutdown_requested());
2933    }
2934
2935    #[test]
2936    fn test_handle_shutdown_signal_helper() {
2937        let shutdown =
2938            Arc::new(ShutdownSignal::new(Duration::from_secs(1)));
2939        let slot =
2940            SHUTDOWN_SIGNAL_SLOT.get_or_init(|| Mutex::new(None));
2941        if let Ok(mut guard) = slot.lock() {
2942            *guard = Some(shutdown.clone());
2943        }
2944        Server::handle_shutdown_signal();
2945        assert!(shutdown.is_shutdown_requested());
2946    }
2947
2948    #[test]
2949    fn test_accept_loop_helpers_cover_ok_and_err_paths() {
2950        let root = setup_test_directory();
2951        let server = Server::builder()
2952            .address("127.0.0.1:0")
2953            .document_root(root.path().to_str().expect("path"))
2954            .build()
2955            .expect("server");
2956
2957        Server::run_basic_accept_loop(
2958            vec![Err(io::Error::other("incoming failed"))],
2959            server.clone(),
2960        );
2961        let pool = ThreadPool::new(1);
2962        Server::run_thread_pool_accept_loop(
2963            vec![Err(io::Error::other("incoming failed"))],
2964            server.clone(),
2965            &pool,
2966        );
2967        Server::run_pooling_accept_loop(
2968            vec![Err(io::Error::other("incoming failed"))],
2969            server.clone(),
2970            &pool,
2971            Arc::new(ConnectionPool::new(1)),
2972        );
2973
2974        let (server_stream, mut client) = connected_pair();
2975        client.write_all(b"BAD\r\n\r\n").expect("write");
2976        Server::run_basic_accept_loop(
2977            vec![Ok(server_stream)],
2978            server.clone(),
2979        );
2980        let mut response = String::new();
2981        let _ = client.read_to_string(&mut response).expect("read");
2982        assert!(response.starts_with("HTTP/1.1 400 BAD REQUEST"));
2983
2984        let (server_stream, mut client) = connected_pair();
2985        client.write_all(b"BAD\r\n\r\n").expect("write");
2986        Server::run_thread_pool_accept_loop(
2987            vec![Ok(server_stream)],
2988            server.clone(),
2989            &pool,
2990        );
2991        let mut response = String::new();
2992        let _ = client.read_to_string(&mut response).expect("read");
2993        assert!(response.starts_with("HTTP/1.1 400 BAD REQUEST"));
2994
2995        let (server_stream, mut client) = connected_pair();
2996        client.write_all(b"BAD\r\n\r\n").expect("write");
2997        Server::run_pooling_accept_loop(
2998            vec![Ok(server_stream)],
2999            server.clone(),
3000            &pool,
3001            Arc::new(ConnectionPool::new(1)),
3002        );
3003        let mut response = String::new();
3004        let _ = client.read_to_string(&mut response).expect("read");
3005        assert!(response.starts_with("HTTP/1.1 400 BAD REQUEST"));
3006
3007        let (server_stream, mut client) = connected_pair();
3008        Server::run_pooling_accept_loop(
3009            vec![Ok(server_stream)],
3010            server,
3011            &pool,
3012            Arc::new(ConnectionPool::new(0)),
3013        );
3014        let mut response = String::new();
3015        let _ = client.read_to_string(&mut response).expect("read");
3016        assert!(
3017            response.starts_with("HTTP/1.1 503 SERVICE UNAVAILABLE")
3018        );
3019    }
3020
3021    #[test]
3022    fn test_immutable_cache_control_policy() {
3023        let root = setup_test_directory();
3024        let server = Server::builder()
3025            .address("127.0.0.1:0")
3026            .document_root(root.path().to_str().expect("path"))
3027            .static_cache_ttl_secs(60)
3028            .build()
3029            .expect("server");
3030
3031        let request = Request {
3032            method: "GET".to_string(),
3033            path: "/assets/app-abcdef12.js".to_string(),
3034            version: "HTTP/1.1".to_string(),
3035            headers: Vec::new(),
3036        };
3037        let response = apply_response_policies(
3038            Response::new(200, "OK", b"ok".to_vec()),
3039            &server,
3040            &request,
3041        );
3042        assert!(response.headers.iter().any(|(name, value)| {
3043            name.eq_ignore_ascii_case("cache-control")
3044                && value.contains("immutable")
3045        }));
3046    }
3047
3048    #[test]
3049    fn test_zstd_precompressed_asset_is_served() {
3050        let root = setup_test_directory();
3051        let file = root.path().join("index.html.zst");
3052        fs::write(&file, b"zstd-data").expect("write");
3053
3054        let headers = vec![(
3055            "accept-encoding".to_string(),
3056            "zstd,gzip".to_string(),
3057        )];
3058        let request = Request {
3059            method: "GET".to_string(),
3060            path: "/index.html".to_string(),
3061            version: "HTTP/1.1".to_string(),
3062            headers,
3063        };
3064
3065        let response = generate_response_with_cache(
3066            &request,
3067            root.path(),
3068            &fs::canonicalize(root.path()).expect("canonicalize"),
3069            None,
3070            DEFAULT_MAX_BUFFERED_BODY_BYTES,
3071        )
3072        .expect("response");
3073        assert!(response.headers.iter().any(|(name, value)| {
3074            name.eq_ignore_ascii_case("content-encoding")
3075                && value.eq_ignore_ascii_case("zstd")
3076        }));
3077        assert_eq!(response.body, b"zstd-data");
3078    }
3079
3080    #[test]
3081    fn test_brotli_precompressed_asset_is_served() {
3082        let root = setup_test_directory();
3083        fs::write(root.path().join("index.html.br"), b"brotli-encoded")
3084            .expect("write br");
3085
3086        let headers = vec![(
3087            "accept-encoding".to_string(),
3088            "br, gzip".to_string(),
3089        )];
3090        let request = Request {
3091            method: "GET".to_string(),
3092            path: "/index.html".to_string(),
3093            version: "HTTP/1.1".to_string(),
3094            headers,
3095        };
3096
3097        let response = generate_response_with_cache(
3098            &request,
3099            root.path(),
3100            &fs::canonicalize(root.path()).expect("canonicalize"),
3101            None,
3102            DEFAULT_MAX_BUFFERED_BODY_BYTES,
3103        )
3104        .expect("response");
3105        assert!(response.headers.iter().any(|(name, value)| {
3106            name.eq_ignore_ascii_case("content-encoding")
3107                && value.eq_ignore_ascii_case("br")
3108        }));
3109        assert_eq!(response.body, b"brotli-encoded");
3110    }
3111
3112    #[test]
3113    fn test_gzip_precompressed_asset_is_served() {
3114        let root = setup_test_directory();
3115        fs::write(root.path().join("index.html.gz"), b"gzdata")
3116            .expect("write gz");
3117
3118        let headers =
3119            vec![("accept-encoding".to_string(), "gzip".to_string())];
3120        let request = Request {
3121            method: "GET".to_string(),
3122            path: "/index.html".to_string(),
3123            version: "HTTP/1.1".to_string(),
3124            headers,
3125        };
3126
3127        let response = generate_response_with_cache(
3128            &request,
3129            root.path(),
3130            &fs::canonicalize(root.path()).expect("canonicalize"),
3131            None,
3132            DEFAULT_MAX_BUFFERED_BODY_BYTES,
3133        )
3134        .expect("response");
3135        assert!(response.headers.iter().any(|(name, value)| {
3136            name.eq_ignore_ascii_case("content-encoding")
3137                && value.eq_ignore_ascii_case("gzip")
3138        }));
3139        assert_eq!(response.body, b"gzdata");
3140    }
3141
3142    #[test]
3143    fn test_serve_file_response_applies_cache_ttl() {
3144        let root = setup_test_directory();
3145        let request = Request {
3146            method: "GET".to_string(),
3147            path: "/index.html".to_string(),
3148            version: "HTTP/1.1".to_string(),
3149            headers: Vec::new(),
3150        };
3151
3152        let response = generate_response_with_cache(
3153            &request,
3154            root.path(),
3155            &fs::canonicalize(root.path()).expect("canonicalize"),
3156            Some(600),
3157            DEFAULT_MAX_BUFFERED_BODY_BYTES,
3158        )
3159        .expect("response");
3160        assert!(response.headers.iter().any(|(name, value)| {
3161            name.eq_ignore_ascii_case("cache-control")
3162                && value.contains("max-age=600")
3163        }));
3164    }
3165
3166    #[test]
3167    fn test_parse_range_header_covers_all_branches() {
3168        // Missing header / wrong prefix / malformed.
3169        assert!(parse_range_header(None, 100).is_none());
3170        assert!(parse_range_header(Some("items=0-1"), 100).is_none());
3171        assert!(
3172            parse_range_header(Some("bytes=no-dash"), 100).is_none()
3173        );
3174        // Both ends empty.
3175        assert!(parse_range_header(Some("bytes=-"), 100).is_none());
3176        // Suffix form: last N bytes.
3177        assert_eq!(
3178            parse_range_header(Some("bytes=-10"), 100),
3179            Some((90, 99))
3180        );
3181        // Suffix longer than file or zero: rejected.
3182        assert!(parse_range_header(Some("bytes=-0"), 100).is_none());
3183        assert!(parse_range_header(Some("bytes=-500"), 100).is_none());
3184        // Open-ended "start-" form uses total-1 as end.
3185        assert_eq!(
3186            parse_range_header(Some("bytes=10-"), 100),
3187            Some((10, 99))
3188        );
3189        // Open-ended on empty body falls off checked_sub.
3190        assert!(parse_range_header(Some("bytes=0-"), 0).is_none());
3191        // Explicit start > end is rejected.
3192        assert!(parse_range_header(Some("bytes=50-10"), 100).is_none());
3193        // End beyond total is rejected.
3194        assert!(
3195            parse_range_header(Some("bytes=0-9999"), 100).is_none()
3196        );
3197        // Well-formed closed range.
3198        assert_eq!(
3199            parse_range_header(Some("bytes=0-9"), 100),
3200            Some((0, 9))
3201        );
3202        // Non-numeric parts.
3203        assert!(parse_range_header(Some("bytes=abc-9"), 100).is_none());
3204        assert!(parse_range_header(Some("bytes=0-abc"), 100).is_none());
3205        assert!(parse_range_header(Some("bytes=-abc"), 100).is_none());
3206    }
3207
3208    #[test]
3209    fn test_non_immutable_cache_control_policy_uses_ttl() {
3210        let root = setup_test_directory();
3211        let server = Server::builder()
3212            .address("127.0.0.1:0")
3213            .document_root(root.path().to_str().expect("path"))
3214            .static_cache_ttl_secs(90)
3215            .build()
3216            .expect("server");
3217
3218        let request = Request {
3219            method: "GET".to_string(),
3220            path: "/index.html".to_string(),
3221            version: "HTTP/1.1".to_string(),
3222            headers: Vec::new(),
3223        };
3224        let response = apply_response_policies(
3225            Response::new(200, "OK", b"ok".to_vec()),
3226            &server,
3227            &request,
3228        );
3229        assert!(response.headers.iter().any(|(name, value)| {
3230            name.eq_ignore_ascii_case("cache-control")
3231                && value == "public, max-age=90"
3232        }));
3233    }
3234
3235    #[test]
3236    fn test_cache_control_policy_respects_existing_header() {
3237        let root = setup_test_directory();
3238        let server = Server::builder()
3239            .address("127.0.0.1:0")
3240            .document_root(root.path().to_str().expect("path"))
3241            .static_cache_ttl_secs(90)
3242            .build()
3243            .expect("server");
3244
3245        let mut existing = Response::new(200, "OK", b"ok".to_vec());
3246        existing.add_header("Cache-Control", "no-store");
3247
3248        let request = Request {
3249            method: "GET".to_string(),
3250            path: "/anything.txt".to_string(),
3251            version: "HTTP/1.1".to_string(),
3252            headers: Vec::new(),
3253        };
3254        let response =
3255            apply_response_policies(existing, &server, &request);
3256        let header = response
3257            .headers
3258            .iter()
3259            .find(|(name, _)| {
3260                name.eq_ignore_ascii_case("cache-control")
3261            })
3262            .map(|(_, value)| value.clone())
3263            .expect("cache-control");
3264        assert_eq!(header, "no-store");
3265    }
3266
3267    #[test]
3268    fn test_is_probably_immutable_asset_path_edge_cases() {
3269        assert!(is_probably_immutable_asset_path(
3270            "/assets/app-abcdef12.js"
3271        ));
3272        // No extension → rsplit_once('.') returns None.
3273        assert!(!is_probably_immutable_asset_path("/noext"));
3274        // Non-hex hash suffix is rejected.
3275        assert!(!is_probably_immutable_asset_path(
3276            "/assets/app-zzzzzzzz.js"
3277        ));
3278        // Too short to be a hash.
3279        assert!(!is_probably_immutable_asset_path("/assets/app-ab.js"));
3280    }
3281
3282    #[test]
3283    fn test_record_metrics_tracks_5xx_responses() {
3284        let before = METRIC_RESPONSES_5XX.load(Ordering::Relaxed);
3285        let response =
3286            Response::new(503, "SERVICE UNAVAILABLE", b"down".to_vec());
3287        record_metrics(&response);
3288        let after = METRIC_RESPONSES_5XX.load(Ordering::Relaxed);
3289        assert!(after > before);
3290    }
3291
3292    #[test]
3293    fn test_rate_limit_recovers_from_poisoned_mutex() {
3294        // Poison only the shard that this IP would route to so the test
3295        // doesn't disturb adjacent shards used by other concurrent tests.
3296        let ip: IpAddr = "127.0.0.1".parse().expect("ip");
3297        let shard = &rate_limit_table()[rate_limit_shard_index(ip)];
3298        let _ = std::panic::catch_unwind(|| {
3299            let _guard = shard.lock().expect("lock");
3300            panic!("intentional to poison");
3301        });
3302        assert!(shard.is_poisoned());
3303
3304        let root = setup_test_directory();
3305        let server = Server::builder()
3306            .address("127.0.0.1:0")
3307            .document_root(root.path().to_str().expect("path"))
3308            .rate_limit_per_minute(10)
3309            .build()
3310            .expect("server");
3311        // Must not panic even though the shard is poisoned — poisoned
3312        // lock recovery branch.
3313        let _ = is_rate_limited(&server, ip);
3314
3315        // Clear poison so subsequent tests see a healthy lock.
3316        shard.clear_poison();
3317    }
3318
3319    #[test]
3320    fn test_log_connection_result_handles_error() {
3321        // The Ok path is exercised by existing tests via run_*_accept_loop.
3322        // Exercise the Err branch directly (eprintln) to cover the error arm.
3323        Server::log_connection_result(Err(
3324            ServerError::invalid_request("boom"),
3325        ));
3326    }
3327
3328    #[test]
3329    fn test_start_with_shutdown_signal_reports_active_connections_on_timeout()
3330     {
3331        let root = setup_test_directory();
3332        let server = Server::builder()
3333            .address("127.0.0.1:0")
3334            .document_root(root.path().to_str().expect("path"))
3335            .build()
3336            .expect("server");
3337
3338        // 50ms grace period so wait_for_shutdown returns `false` if a
3339        // connection is still in flight when we request shutdown.
3340        let shutdown =
3341            Arc::new(ShutdownSignal::new(Duration::from_millis(50)));
3342        let (ready_tx, ready_rx) = mpsc::channel::<String>();
3343        let shutdown_for_server = shutdown.clone();
3344        let server_clone = server.clone();
3345        let handle = thread::spawn(move || {
3346            server_clone
3347                .start_with_shutdown_signal_and_ready(
3348                    shutdown_for_server,
3349                    move |addr| {
3350                        let _ = ready_tx.send(addr);
3351                    },
3352                )
3353                .expect("server start");
3354        });
3355
3356        let addr = ready_rx
3357            .recv_timeout(Duration::from_secs(2))
3358            .expect("ready");
3359
3360        // Hold a long-running connection so the grace period expires before
3361        // the tracked handler finishes, forcing the "active connections
3362        // remaining" branch.
3363        let _holder = TcpStream::connect(&addr).expect("connect");
3364        thread::sleep(Duration::from_millis(20));
3365        shutdown.shutdown();
3366
3367        handle.join().expect("join server thread");
3368    }
3369
3370    #[test]
3371    fn test_start_with_thread_pool_serves_one_connection() {
3372        let root = setup_test_directory();
3373        let probe = TcpListener::bind("127.0.0.1:0").expect("probe");
3374        let addr = probe.local_addr().expect("addr");
3375        drop(probe);
3376
3377        let server = Server::builder()
3378            .address(&addr.to_string())
3379            .document_root(root.path().to_str().expect("path"))
3380            .build()
3381            .expect("server");
3382
3383        let _handle = thread::spawn(move || {
3384            let _ = server.start_with_thread_pool(2);
3385        });
3386
3387        // Retry briefly until the server has bound.
3388        let mut stream = None;
3389        for _ in 0..50 {
3390            if let Ok(s) = TcpStream::connect(addr.to_string()) {
3391                stream = Some(s);
3392                break;
3393            }
3394            thread::sleep(Duration::from_millis(20));
3395        }
3396        let mut stream = stream.expect("server did not bind");
3397        stream
3398            .write_all(
3399                b"GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n",
3400            )
3401            .expect("write");
3402        let mut response = String::new();
3403        let _ = stream.read_to_string(&mut response).expect("read");
3404        assert!(response.starts_with("HTTP/1.1 200 OK"));
3405        // Thread continues serving but detaches with the test.
3406    }
3407
3408    #[test]
3409    fn test_start_with_pooling_serves_one_connection() {
3410        let root = setup_test_directory();
3411        let probe = TcpListener::bind("127.0.0.1:0").expect("probe");
3412        let addr = probe.local_addr().expect("addr");
3413        drop(probe);
3414
3415        let server = Server::builder()
3416            .address(&addr.to_string())
3417            .document_root(root.path().to_str().expect("path"))
3418            .build()
3419            .expect("server");
3420
3421        let _handle = thread::spawn(move || {
3422            let _ = server.start_with_pooling(2, 4);
3423        });
3424
3425        let mut stream = None;
3426        for _ in 0..50 {
3427            if let Ok(s) = TcpStream::connect(addr.to_string()) {
3428                stream = Some(s);
3429                break;
3430            }
3431            thread::sleep(Duration::from_millis(20));
3432        }
3433        let mut stream = stream.expect("server did not bind");
3434        stream
3435            .write_all(
3436                b"GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n",
3437            )
3438            .expect("write");
3439        let mut response = String::new();
3440        let _ = stream.read_to_string(&mut response).expect("read");
3441        assert!(response.starts_with("HTTP/1.1 200 OK"));
3442    }
3443
3444    // ── Coverage fillers ────────────────────────────────────────────
3445    // The tests below exist to push individual files past the 98 %
3446    // line-coverage threshold. They are deliberately small and
3447    // targeted: each names the lines or branch it covers in a comment
3448    // so future churn keeps the coverage explanation co-located with
3449    // the test.
3450
3451    /// Covers `ServerBuilder::disable_cors`.
3452    #[test]
3453    fn test_server_builder_disable_cors_setter() {
3454        let server = Server::builder()
3455            .address("127.0.0.1:0")
3456            .document_root(".")
3457            .enable_cors()
3458            .disable_cors()
3459            .build()
3460            .expect("server");
3461        assert_eq!(server.cors_enabled(), Some(false));
3462    }
3463
3464    /// Covers `ServerBuilder::max_buffered_body_bytes` and the matching
3465    /// `Server::max_buffered_body_bytes` getter override path.
3466    #[test]
3467    fn test_server_builder_max_buffered_body_bytes_override() {
3468        let server = Server::builder()
3469            .address("127.0.0.1:0")
3470            .document_root(".")
3471            .max_buffered_body_bytes(1_000_000)
3472            .build()
3473            .expect("server");
3474        assert_eq!(server.max_buffered_body_bytes(), 1_000_000);
3475    }
3476
3477    /// Covers `Server::max_buffered_body_bytes` default-fallback path.
3478    #[test]
3479    fn test_server_max_buffered_body_bytes_default_fallback() {
3480        let server = Server::builder()
3481            .address("127.0.0.1:0")
3482            .document_root(".")
3483            .build()
3484            .expect("server");
3485        assert_eq!(
3486            server.max_buffered_body_bytes(),
3487            DEFAULT_MAX_BUFFERED_BODY_BYTES
3488        );
3489    }
3490
3491    /// Covers `ShutdownSignal::default`.
3492    #[test]
3493    fn test_shutdown_signal_default_constructor() {
3494        let signal = ShutdownSignal::default();
3495        assert_eq!(signal.shutdown_timeout, Duration::from_secs(30));
3496        assert!(!signal.is_shutdown_requested());
3497    }
3498
3499    /// Covers the `> max_buffered_body_bytes` body-cap error branch in
3500    /// `serve_file_response`. Builds a tiny cap (1 byte) so any non-empty
3501    /// file trips the rejection.
3502    #[test]
3503    fn test_serve_file_response_rejects_oversize_body() {
3504        let root = setup_test_directory();
3505        let request = Request {
3506            method: "GET".to_string(),
3507            path: "/index.html".to_string(),
3508            version: "HTTP/1.1".to_string(),
3509            headers: Vec::new(),
3510        };
3511        let canonical =
3512            fs::canonicalize(root.path()).expect("canonicalize");
3513        let err = generate_response_with_cache(
3514            &request,
3515            root.path(),
3516            &canonical,
3517            None,
3518            1, // 1-byte cap; the test fixture writes a multi-byte file
3519        )
3520        .expect_err("oversized file must be rejected");
3521        assert!(
3522            err.to_string().contains("exceeds in-memory serve cap"),
3523            "unexpected error: {err}"
3524        );
3525    }
3526
3527    /// Covers the etag cache eviction path (drop_count loop) by inserting
3528    /// more than ETAG_CACHE_MAX synthetic entries directly into the
3529    /// global cache and then forcing one more `compute_etag` call.
3530    #[test]
3531    fn test_etag_cache_evicts_when_full() {
3532        let cache = etag_cache();
3533        // Pre-fill past the cap with synthetic entries.
3534        if let Ok(mut write) = cache.write() {
3535            for i in 0..(ETAG_CACHE_MAX + 1) as u64 {
3536                let _ = write.insert(
3537                    (i, i),
3538                    Arc::<str>::from(format!("W/\"{i:x}-{i:x}\"")),
3539                );
3540            }
3541        }
3542        // Sanity: cache is at-or-above the cap.
3543        let len_before =
3544            cache.read().map(|r| r.len()).unwrap_or_default();
3545        assert!(len_before >= ETAG_CACHE_MAX);
3546
3547        // Trigger a real compute_etag — this hits the eviction branch
3548        // because the cache is full and the new key (1, 0) isn't a
3549        // pre-seeded entry.
3550        let temp = std::env::temp_dir();
3551        let metadata = fs::metadata(&temp).expect("metadata");
3552        let _ = compute_etag(&metadata);
3553
3554        let len_after =
3555            cache.read().map(|r| r.len()).unwrap_or_default();
3556        // Eviction dropped a quarter, so the cache must be smaller
3557        // than the worst-case pre-fill state.
3558        assert!(
3559            len_after <= ETAG_CACHE_MAX,
3560            "cache len {len_after} exceeds cap {ETAG_CACHE_MAX}"
3561        );
3562    }
3563
3564    /// Covers the HTTP/1.0 + explicit `Connection: keep-alive` arm and
3565    /// the HTTP/1.0-without-header default-close arm of
3566    /// [`ConnectionPolicy::from_request`]. The HTTP/1.1 arms are
3567    /// already exercised end-to-end by the keep-alive integration
3568    /// tests; these two arms only fire on legacy HTTP/1.0 traffic and
3569    /// would otherwise stay uncovered.
3570    #[test]
3571    fn connection_policy_handles_http_1_0_explicit_keepalive_and_default_close()
3572     {
3573        let keepalive = Request {
3574            method: "GET".into(),
3575            path: "/".into(),
3576            version: "HTTP/1.0".into(),
3577            headers: vec![("connection".into(), "keep-alive".into())],
3578        };
3579        assert_eq!(
3580            ConnectionPolicy::from_request(&keepalive),
3581            ConnectionPolicy::KeepAlive
3582        );
3583
3584        let bare = Request {
3585            method: "GET".into(),
3586            path: "/".into(),
3587            version: "HTTP/1.0".into(),
3588            headers: Vec::new(),
3589        };
3590        assert_eq!(
3591            ConnectionPolicy::from_request(&bare),
3592            ConnectionPolicy::Close
3593        );
3594    }
3595
3596    /// Covers the empty-fallback branch of
3597    /// [`Server::canonical_document_root`]. The accessor falls back to
3598    /// `document_root` when the cached canonical path is empty, which
3599    /// happens for `Server` values reconstructed via `Default` (e.g.
3600    /// after `serde::Deserialize` since `canonical_document_root` is
3601    /// `#[serde(skip)]`).
3602    #[test]
3603    fn canonical_document_root_falls_back_when_cache_is_empty() {
3604        // `canonical_document_root` is `#[serde(skip, default)]`, so a
3605        // freshly-defaulted `Server` has it empty. Construct via
3606        // struct-literal so the empty-cache state is set at build
3607        // time rather than mutated post-`Default` (clippy's
3608        // `field_reassign_with_default` flags the latter).
3609        let mut server = Server {
3610            document_root: PathBuf::from("/tmp/some-root"),
3611            canonical_document_root: PathBuf::new(),
3612            ..Server::default()
3613        };
3614        assert_eq!(
3615            server.canonical_document_root(),
3616            Path::new("/tmp/some-root")
3617        );
3618
3619        // When the cache is populated the accessor returns it
3620        // unchanged, not the configured `document_root`.
3621        server.canonical_document_root =
3622            PathBuf::from("/canonical/elsewhere");
3623        assert_eq!(
3624            server.canonical_document_root(),
3625            Path::new("/canonical/elsewhere")
3626        );
3627    }
3628}