Skip to main content

http_handle/
perf_server.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (c) 2026 Sebastien Rousseau
3
4//! High-performance async-first HTTP/1 server primitives.
5
6#[cfg(feature = "high-perf")]
7#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
8use crate::error::ServerError;
9#[cfg(feature = "high-perf")]
10#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
11use crate::request::Request;
12#[cfg(feature = "high-perf")]
13#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
14use crate::response::Response;
15#[cfg(feature = "high-perf")]
16#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
17use crate::server::{Server, build_response_for_request_with_metrics};
18
19#[cfg(feature = "high-perf")]
20#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
21use std::collections::HashMap;
22#[cfg(feature = "high-perf")]
23#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
24use std::path::{Path, PathBuf};
25#[cfg(feature = "high-perf")]
26#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
27use std::sync::Arc;
28#[cfg(feature = "high-perf")]
29#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
30use std::sync::atomic::{AtomicUsize, Ordering};
31
32#[cfg(feature = "high-perf")]
33#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
34use tokio::io::{AsyncReadExt, AsyncWriteExt};
35#[cfg(feature = "high-perf")]
36#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
37use tokio::sync::Semaphore;
38#[cfg(feature = "high-perf")]
39#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
40use tokio::time::{Duration, timeout};
41
42/// Runtime limits for the high-performance server mode.
43#[cfg(feature = "high-perf")]
44#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
45///
46/// # Examples
47///
48/// ```rust
49/// use http_handle::perf_server::PerfLimits;
50/// let limits = PerfLimits::default();
51/// assert!(limits.max_inflight > 0);
52/// ```
53///
54/// # Panics
55///
56/// This type does not panic.
57#[derive(Clone, Copy, Debug)]
58pub struct PerfLimits {
59    /// Maximum number of concurrently processed connections.
60    pub max_inflight: usize,
61    /// Maximum number of queued connections waiting for a slot.
62    pub max_queue: usize,
63    /// Minimum file size (bytes) for sendfile fast-path attempts.
64    pub sendfile_threshold_bytes: u64,
65}
66
67#[cfg(feature = "high-perf")]
68#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
69impl Default for PerfLimits {
70    fn default() -> Self {
71        Self {
72            max_inflight: 256,
73            max_queue: 1024,
74            sendfile_threshold_bytes: 64 * 1024,
75        }
76    }
77}
78
79/// Starts an async-first accept loop with adaptive backpressure.
80///
81/// This path prioritizes throughput-per-core by avoiding a thread-per-connection model,
82/// enforcing queue limits, and using a sendfile fast-path for large static files.
83#[cfg(feature = "high-perf")]
84#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
85///
86/// # Examples
87///
88/// ```rust,no_run
89/// use http_handle::perf_server::{start_high_perf, PerfLimits};
90/// use http_handle::Server;
91/// # #[tokio::main(flavor = "current_thread")]
92/// # async fn main() {
93/// let server = Server::new("127.0.0.1:8080", ".");
94/// let _ = start_high_perf(server, PerfLimits::default()).await;
95/// # }
96/// ```
97///
98/// # Errors
99///
100/// Returns an error when socket binding or accept fails.
101///
102/// # Panics
103///
104/// This function does not panic.
105pub async fn start_high_perf(
106    server: Server,
107    limits: PerfLimits,
108) -> Result<(), ServerError> {
109    let listener = tokio::net::TcpListener::bind(server.address())
110        .await
111        .map_err(ServerError::from)?;
112
113    let inflight = Arc::new(Semaphore::new(limits.max_inflight.max(1)));
114    let queued = Arc::new(AtomicUsize::new(0));
115
116    loop {
117        let (stream, _addr) =
118            listener.accept().await.map_err(ServerError::from)?;
119
120        let permit = match inflight.clone().try_acquire_owned() {
121            Ok(permit) => permit,
122            Err(_) => {
123                let queued_now =
124                    queued.fetch_add(1, Ordering::SeqCst) + 1;
125                if queued_now > limits.max_queue {
126                    let _ = queued.fetch_sub(1, Ordering::SeqCst);
127                    continue;
128                }
129                let acquired = timeout(
130                    Duration::from_millis(20),
131                    inflight.clone().acquire_owned(),
132                )
133                .await;
134                let _ = queued.fetch_sub(1, Ordering::SeqCst);
135                match acquired {
136                    Ok(Ok(permit)) => permit,
137                    _ => continue,
138                }
139            }
140        };
141
142        let server_clone = server.clone();
143        let limits_clone = limits;
144        drop(tokio::spawn(async move {
145            let _permit = permit;
146            let _ = handle_async_connection(
147                stream,
148                &server_clone,
149                &limits_clone,
150            )
151            .await;
152        }));
153    }
154}
155
156#[cfg(feature = "high-perf")]
157#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
158async fn handle_async_connection(
159    mut stream: tokio::net::TcpStream,
160    server: &Server,
161    limits: &PerfLimits,
162) -> Result<(), ServerError> {
163    let timeout_dur =
164        server.request_timeout().unwrap_or(Duration::from_secs(30));
165
166    let mut buffer = vec![0_u8; 16 * 1024];
167    let read = timeout(timeout_dur, stream.read(&mut buffer))
168        .await
169        .map_err(|_| ServerError::invalid_request("request timeout"))?
170        .map_err(ServerError::from)?;
171
172    if read == 0 {
173        return Ok(());
174    }
175    buffer.truncate(read);
176
177    let request = parse_request_from_bytes(&buffer)?;
178
179    if try_send_static_file_fast_path(
180        &mut stream,
181        server,
182        &request,
183        limits.sendfile_threshold_bytes,
184    )
185    .await?
186    {
187        return Ok(());
188    }
189
190    let response =
191        build_response_for_request_with_metrics(server, &request);
192    send_response_async(&mut stream, &response).await
193}
194
195#[cfg(feature = "high-perf")]
196#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
197fn parse_request_from_bytes(
198    bytes: &[u8],
199) -> Result<Request, ServerError> {
200    let text = std::str::from_utf8(bytes).map_err(|_| {
201        ServerError::invalid_request("request is not valid UTF-8")
202    })?;
203    let (head, _) = text.split_once("\r\n\r\n").ok_or_else(|| {
204        ServerError::invalid_request("incomplete HTTP request head")
205    })?;
206
207    let mut lines = head.lines();
208    let request_line = lines.next().ok_or_else(|| {
209        ServerError::invalid_request("missing request line")
210    })?;
211    let mut parts = request_line.split_whitespace();
212    let method = parts
213        .next()
214        .ok_or_else(|| ServerError::invalid_request("missing method"))?
215        .to_string();
216    let path = parts
217        .next()
218        .ok_or_else(|| ServerError::invalid_request("missing path"))?
219        .to_string();
220    let version = parts
221        .next()
222        .ok_or_else(|| {
223            ServerError::invalid_request("missing HTTP version")
224        })?
225        .to_string();
226
227    let mut headers = HashMap::new();
228    for line in lines {
229        if line.is_empty() {
230            break;
231        }
232        if let Some((name, value)) = line.split_once(':') {
233            let _ = headers.insert(
234                name.trim().to_ascii_lowercase(),
235                value.trim().to_string(),
236            );
237        }
238    }
239
240    Ok(Request {
241        method,
242        path,
243        version,
244        headers,
245    })
246}
247
248#[cfg(feature = "high-perf")]
249#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
250async fn send_response_async(
251    stream: &mut tokio::net::TcpStream,
252    response: &Response,
253) -> Result<(), ServerError> {
254    let mut header = format!(
255        "HTTP/1.1 {} {}\r\n",
256        response.status_code, response.status_text
257    );
258
259    let mut has_content_length = false;
260    let mut has_connection = false;
261    for (name, value) in &response.headers {
262        if name.eq_ignore_ascii_case("content-length") {
263            has_content_length = true;
264        }
265        if name.eq_ignore_ascii_case("connection") {
266            has_connection = true;
267        }
268        header.push_str(&format!("{}: {}\r\n", name, value));
269    }
270    if !has_content_length {
271        header.push_str(&format!(
272            "Content-Length: {}\r\n",
273            response.body.len()
274        ));
275    }
276    if !has_connection {
277        header.push_str("Connection: close\r\n");
278    }
279    header.push_str("\r\n");
280
281    stream
282        .write_all(header.as_bytes())
283        .await
284        .map_err(ServerError::from)?;
285    if !response.body.is_empty() {
286        stream
287            .write_all(&response.body)
288            .await
289            .map_err(ServerError::from)?;
290    }
291    stream.flush().await.map_err(ServerError::from)?;
292    Ok(())
293}
294
295#[cfg(feature = "high-perf")]
296#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
297async fn try_send_static_file_fast_path(
298    stream: &mut tokio::net::TcpStream,
299    server: &Server,
300    request: &Request,
301    sendfile_threshold_bytes: u64,
302) -> Result<bool, ServerError> {
303    if request.method() != "GET" && request.method() != "HEAD" {
304        return Ok(false);
305    }
306    if request.header("range").is_some() {
307        return Ok(false);
308    }
309
310    let Some(file_path) =
311        resolve_static_path(server.document_root(), request.path())
312    else {
313        return Ok(false);
314    };
315
316    let (serving_path, encoding) =
317        negotiate_precompressed(&file_path, request);
318    let metadata =
319        std::fs::metadata(&serving_path).map_err(ServerError::from)?;
320    let len = metadata.len();
321
322    let mut headers = Vec::new();
323    headers.push(("Content-Type", content_type_for_path(&file_path)));
324    headers.push(("Accept-Ranges", "bytes"));
325    if let Some(enc) = encoding {
326        headers.push(("Content-Encoding", enc));
327        headers.push(("Vary", "Accept-Encoding"));
328    }
329    if is_probably_immutable_asset(request.path()) {
330        headers.push((
331            "Cache-Control",
332            "public, max-age=31536000, immutable",
333        ));
334    }
335
336    let mut head = format!(
337        "HTTP/1.1 200 OK\r\nContent-Length: {len}\r\nConnection: close\r\n"
338    );
339    for (name, value) in headers {
340        head.push_str(name);
341        head.push_str(": ");
342        head.push_str(value);
343        head.push_str("\r\n");
344    }
345    head.push_str("\r\n");
346
347    stream
348        .write_all(head.as_bytes())
349        .await
350        .map_err(ServerError::from)?;
351
352    if request.method() == "HEAD" {
353        stream.flush().await.map_err(ServerError::from)?;
354        return Ok(true);
355    }
356
357    if len >= sendfile_threshold_bytes {
358        #[cfg(unix)]
359        {
360            if try_sendfile_unix(stream, &serving_path, len).await? {
361                stream.flush().await.map_err(ServerError::from)?;
362                return Ok(true);
363            }
364        }
365    }
366
367    let mut file = tokio::fs::File::open(&serving_path)
368        .await
369        .map_err(ServerError::from)?;
370    let _bytes_copied = tokio::io::copy(&mut file, stream)
371        .await
372        .map_err(ServerError::from)?;
373    stream.flush().await.map_err(ServerError::from)?;
374    Ok(true)
375}
376
377#[cfg(feature = "high-perf")]
378#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
379fn resolve_static_path(
380    root: &Path,
381    request_path: &str,
382) -> Option<PathBuf> {
383    let canonical_root = std::fs::canonicalize(root).ok()?;
384    let mut path = root.to_path_buf();
385    let rel = request_path.trim_start_matches('/');
386
387    if rel.is_empty() {
388        path.push("index.html");
389    } else {
390        for part in rel.split('/') {
391            if part == ".." {
392                let _ = path.pop();
393            } else {
394                path.push(part);
395            }
396        }
397    }
398
399    let resolved = std::fs::canonicalize(&path).ok()?;
400    if !resolved.starts_with(canonical_root) {
401        return None;
402    }
403
404    if resolved.is_dir() {
405        let index = resolved.join("index.html");
406        if index.is_file() {
407            return Some(index);
408        }
409        return None;
410    }
411
412    if resolved.is_file() {
413        Some(resolved)
414    } else {
415        None
416    }
417}
418
419#[cfg(feature = "high-perf")]
420#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
421fn negotiate_precompressed(
422    path: &Path,
423    request: &Request,
424) -> (PathBuf, Option<&'static str>) {
425    let mut serving_path = path.to_path_buf();
426    let mut encoding = None;
427
428    if let Some(accept) = request.header("accept-encoding") {
429        if accept.contains("br") {
430            let candidate =
431                PathBuf::from(format!("{}.br", path.display()));
432            if candidate.is_file() {
433                serving_path = candidate;
434                encoding = Some("br");
435                return (serving_path, encoding);
436            }
437        }
438        if accept.contains("zstd") || accept.contains("zst") {
439            let candidate =
440                PathBuf::from(format!("{}.zst", path.display()));
441            if candidate.is_file() {
442                serving_path = candidate;
443                encoding = Some("zstd");
444                return (serving_path, encoding);
445            }
446        }
447        if accept.contains("gzip") {
448            let candidate =
449                PathBuf::from(format!("{}.gz", path.display()));
450            if candidate.is_file() {
451                serving_path = candidate;
452                encoding = Some("gzip");
453            }
454        }
455    }
456
457    (serving_path, encoding)
458}
459
460#[cfg(feature = "high-perf")]
461#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
462fn content_type_for_path(path: &Path) -> &'static str {
463    match path
464        .extension()
465        .and_then(|v| v.to_str())
466        .unwrap_or_default()
467    {
468        "html" | "htm" => "text/html",
469        "css" => "text/css",
470        "js" | "mjs" => "application/javascript",
471        "json" => "application/json",
472        "wasm" => "application/wasm",
473        "svg" => "image/svg+xml",
474        "png" => "image/png",
475        "jpg" | "jpeg" => "image/jpeg",
476        "gif" => "image/gif",
477        _ => "application/octet-stream",
478    }
479}
480
481#[cfg(feature = "high-perf")]
482#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
483fn is_probably_immutable_asset(path: &str) -> bool {
484    let file = path.rsplit('/').next().unwrap_or(path);
485    let Some((stem, _ext)) = file.rsplit_once('.') else {
486        return false;
487    };
488    let Some(hash) = stem.rsplit('-').next() else {
489        return false;
490    };
491    hash.len() >= 8 && hash.chars().all(|c| c.is_ascii_hexdigit())
492}
493
494#[cfg(all(
495    feature = "high-perf",
496    any(target_os = "linux", target_os = "android")
497))]
498async fn try_sendfile_unix(
499    stream: &tokio::net::TcpStream,
500    path: &Path,
501    len: u64,
502) -> Result<bool, ServerError> {
503    use std::os::fd::AsRawFd;
504    let file = std::fs::File::open(path).map_err(ServerError::from)?;
505    let mut offset: libc::off_t = 0;
506    let mut sent: u64 = 0;
507
508    while sent < len {
509        let remaining = (len - sent) as usize;
510        let chunk = remaining.min(1 << 20);
511        let rc = unsafe {
512            libc::sendfile(
513                stream.as_raw_fd(),
514                file.as_raw_fd(),
515                &mut offset,
516                chunk,
517            )
518        };
519        if rc == 0 {
520            break;
521        }
522        if rc < 0 {
523            let err = std::io::Error::last_os_error();
524            if matches!(err.raw_os_error(), Some(libc::EAGAIN)) {
525                tokio::task::yield_now().await;
526                continue;
527            }
528            return Ok(false);
529        }
530        sent = sent.saturating_add(rc as u64);
531    }
532
533    Ok(sent > 0)
534}
535
536#[cfg(all(
537    feature = "high-perf",
538    unix,
539    not(any(target_os = "linux", target_os = "android"))
540))]
541async fn try_sendfile_unix(
542    _stream: &tokio::net::TcpStream,
543    _path: &Path,
544    _len: u64,
545) -> Result<bool, ServerError> {
546    Ok(false)
547}
548
549#[cfg(all(test, feature = "high-perf"))]
550mod tests {
551    use super::*;
552    use std::collections::HashMap;
553    use tokio::io::AsyncReadExt;
554    use tokio::io::AsyncWriteExt;
555    use tokio::time::Duration;
556
557    #[test]
558    fn immutable_asset_detection() {
559        assert!(is_probably_immutable_asset("/assets/app-abcdef12.js"));
560        assert!(!is_probably_immutable_asset("/assets/app.js"));
561    }
562
563    #[test]
564    fn parse_request_from_bytes_parses_headers() {
565        let request = parse_request_from_bytes(
566            b"GET / HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n",
567        )
568        .expect("parse");
569        assert_eq!(request.method(), "GET");
570        assert_eq!(request.path(), "/");
571        assert_eq!(request.header("host"), Some("localhost"));
572        assert_eq!(request.header("accept-encoding"), Some("gzip"));
573    }
574
575    #[test]
576    fn parse_request_from_bytes_rejects_invalid_inputs() {
577        assert!(parse_request_from_bytes(b"\xFF").is_err());
578        assert!(
579            parse_request_from_bytes(b"GET / HTTP/1.1\r\n").is_err()
580        );
581        assert!(
582            parse_request_from_bytes(b"/ HTTP/1.1\r\n\r\n").is_err()
583        );
584        assert!(parse_request_from_bytes(b"\r\n\r\n").is_err());
585        assert!(parse_request_from_bytes(b"GET\r\n\r\n").is_err());
586        assert!(parse_request_from_bytes(b"GET / \r\n\r\n").is_err());
587    }
588
589    #[test]
590    fn resolve_static_path_and_content_type_behave() {
591        let dir = tempfile::tempdir().expect("tempdir");
592        let root = dir.path();
593        std::fs::write(root.join("index.html"), "ok").expect("write");
594        std::fs::create_dir(root.join("nested")).expect("mkdir");
595        std::fs::write(root.join("nested").join("index.html"), "n")
596            .expect("write");
597
598        let p1 = resolve_static_path(root, "/").expect("root index");
599        assert!(p1.ends_with("index.html"));
600        let p2 =
601            resolve_static_path(root, "/nested").expect("nested index");
602        assert!(p2.ends_with("nested/index.html"));
603        assert!(
604            resolve_static_path(root, "/../../etc/passwd").is_none()
605        );
606
607        assert_eq!(
608            content_type_for_path(Path::new("a.html")),
609            "text/html"
610        );
611        assert_eq!(
612            content_type_for_path(Path::new("a.css")),
613            "text/css"
614        );
615        assert_eq!(
616            content_type_for_path(Path::new("a.js")),
617            "application/javascript"
618        );
619        assert_eq!(
620            content_type_for_path(Path::new("a.bin")),
621            "application/octet-stream"
622        );
623        assert_eq!(
624            content_type_for_path(Path::new("a.json")),
625            "application/json"
626        );
627        assert_eq!(
628            content_type_for_path(Path::new("a.wasm")),
629            "application/wasm"
630        );
631        assert_eq!(
632            content_type_for_path(Path::new("a.svg")),
633            "image/svg+xml"
634        );
635        assert_eq!(
636            content_type_for_path(Path::new("a.png")),
637            "image/png"
638        );
639        assert_eq!(
640            content_type_for_path(Path::new("a.jpg")),
641            "image/jpeg"
642        );
643        assert_eq!(
644            content_type_for_path(Path::new("a.gif")),
645            "image/gif"
646        );
647    }
648
649    #[test]
650    fn negotiate_precompressed_prefers_br_then_zstd_then_gzip() {
651        let dir = tempfile::tempdir().expect("tempdir");
652        let base = dir.path().join("index.html");
653        std::fs::write(&base, "x").expect("base");
654
655        let mut headers = HashMap::new();
656        let _ = headers
657            .insert("accept-encoding".to_string(), "gzip".to_string());
658        let req_gz = Request {
659            method: "GET".to_string(),
660            path: "/index.html".to_string(),
661            version: "HTTP/1.1".to_string(),
662            headers,
663        };
664        std::fs::write(format!("{}.gz", base.display()), "x")
665            .expect("gz");
666        let (p, e) = negotiate_precompressed(&base, &req_gz);
667        assert!(p.ends_with("index.html.gz"));
668        assert_eq!(e, Some("gzip"));
669
670        std::fs::write(format!("{}.zst", base.display()), "x")
671            .expect("zst");
672        let mut headers = HashMap::new();
673        let _ = headers.insert(
674            "accept-encoding".to_string(),
675            "zstd,gzip".to_string(),
676        );
677        let req_zst = Request {
678            method: "GET".to_string(),
679            path: "/index.html".to_string(),
680            version: "HTTP/1.1".to_string(),
681            headers,
682        };
683        let (p, e) = negotiate_precompressed(&base, &req_zst);
684        assert!(p.ends_with("index.html.zst"));
685        assert_eq!(e, Some("zstd"));
686
687        std::fs::write(format!("{}.br", base.display()), "x")
688            .expect("br");
689        let mut headers = HashMap::new();
690        let _ = headers.insert(
691            "accept-encoding".to_string(),
692            "br,zstd,gzip".to_string(),
693        );
694        let req_br = Request {
695            method: "GET".to_string(),
696            path: "/index.html".to_string(),
697            version: "HTTP/1.1".to_string(),
698            headers,
699        };
700        let (p, e) = negotiate_precompressed(&base, &req_br);
701        assert!(p.ends_with("index.html.br"));
702        assert_eq!(e, Some("br"));
703
704        let mut headers = HashMap::new();
705        let _ = headers
706            .insert("accept-encoding".to_string(), "gzip".to_string());
707        let req_gz_missing = Request {
708            method: "GET".to_string(),
709            path: "/index.html".to_string(),
710            version: "HTTP/1.1".to_string(),
711            headers,
712        };
713        std::fs::remove_file(format!("{}.gz", base.display()))
714            .expect("remove gz");
715        let (p, e) = negotiate_precompressed(&base, &req_gz_missing);
716        assert!(p.ends_with("index.html"));
717        assert_eq!(e, None);
718    }
719
720    #[tokio::test(flavor = "current_thread")]
721    async fn try_send_static_file_fast_path_serves_get_and_head() {
722        let dir = tempfile::tempdir().expect("tempdir");
723        let root = dir.path();
724        std::fs::write(
725            root.join("app-abcdef12.js"),
726            "console.log('ok');",
727        )
728        .expect("write");
729
730        let server = Server::builder()
731            .address("127.0.0.1:0")
732            .document_root(root.to_string_lossy().as_ref())
733            .build()
734            .expect("server");
735        let request = Request {
736            method: "GET".into(),
737            path: "/app-abcdef12.js".into(),
738            version: "HTTP/1.1".into(),
739            headers: HashMap::new(),
740        };
741
742        let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
743            .await
744            .expect("bind");
745        let addr = listener.local_addr().expect("addr");
746        let client_task = tokio::spawn(async move {
747            tokio::net::TcpStream::connect(addr).await.expect("connect")
748        });
749        let (server_stream, _) =
750            listener.accept().await.expect("accept");
751        let mut client = client_task.await.expect("join");
752
753        let server_clone = server.clone();
754        let server_task = tokio::spawn(async move {
755            let mut stream = server_stream;
756            try_send_static_file_fast_path(
757                &mut stream,
758                &server_clone,
759                &request,
760                u64::MAX,
761            )
762            .await
763            .expect("send")
764        });
765
766        let mut bytes = Vec::new();
767        let _ = client.read_to_end(&mut bytes).await.expect("read");
768        assert!(server_task.await.expect("join"));
769
770        let text = String::from_utf8(bytes).expect("utf8");
771        assert!(text.contains("HTTP/1.1 200 OK"));
772        assert!(text.contains(
773            "Cache-Control: public, max-age=31536000, immutable"
774        ));
775        assert!(text.contains("application/javascript"));
776
777        let request_head = Request {
778            method: "HEAD".into(),
779            path: "/app-abcdef12.js".into(),
780            version: "HTTP/1.1".into(),
781            headers: HashMap::new(),
782        };
783
784        let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
785            .await
786            .expect("bind");
787        let addr = listener.local_addr().expect("addr");
788        let client_task = tokio::spawn(async move {
789            tokio::net::TcpStream::connect(addr).await.expect("connect")
790        });
791        let (server_stream, _) =
792            listener.accept().await.expect("accept");
793        let mut client = client_task.await.expect("join");
794        let server_clone = server.clone();
795        let server_task = tokio::spawn(async move {
796            let mut stream = server_stream;
797            try_send_static_file_fast_path(
798                &mut stream,
799                &server_clone,
800                &request_head,
801                u64::MAX,
802            )
803            .await
804            .expect("send")
805        });
806        let mut bytes = Vec::new();
807        let _ = client.read_to_end(&mut bytes).await.expect("read");
808        assert!(server_task.await.expect("join"));
809        let text = String::from_utf8(bytes).expect("utf8");
810        assert!(text.contains("HTTP/1.1 200 OK"));
811        assert!(!text.contains("console.log('ok')"));
812    }
813
814    #[tokio::test(flavor = "current_thread")]
815    async fn try_send_static_file_fast_path_rejects_non_get_and_range()
816    {
817        let dir = tempfile::tempdir().expect("tempdir");
818        let root = dir.path();
819        std::fs::write(root.join("index.html"), "ok").expect("write");
820
821        let server = Server::builder()
822            .address("127.0.0.1:0")
823            .document_root(root.to_string_lossy().as_ref())
824            .build()
825            .expect("server");
826
827        let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
828            .await
829            .expect("bind");
830        let addr = listener.local_addr().expect("addr");
831        let client_task = tokio::spawn(async move {
832            tokio::net::TcpStream::connect(addr).await.expect("connect")
833        });
834        let (mut server_stream, _) =
835            listener.accept().await.expect("accept");
836        let _client = client_task.await.expect("join");
837
838        let post_req = Request {
839            method: "POST".into(),
840            path: "/index.html".into(),
841            version: "HTTP/1.1".into(),
842            headers: HashMap::new(),
843        };
844        assert!(
845            !try_send_static_file_fast_path(
846                &mut server_stream,
847                &server,
848                &post_req,
849                u64::MAX
850            )
851            .await
852            .expect("ok")
853        );
854
855        let mut headers = HashMap::new();
856        let _ = headers.insert("range".into(), "bytes=0-3".into());
857        let range_req = Request {
858            method: "GET".into(),
859            path: "/index.html".into(),
860            version: "HTTP/1.1".into(),
861            headers,
862        };
863        assert!(
864            !try_send_static_file_fast_path(
865                &mut server_stream,
866                &server,
867                &range_req,
868                u64::MAX
869            )
870            .await
871            .expect("ok")
872        );
873    }
874
875    #[tokio::test(flavor = "current_thread")]
876    async fn send_response_async_adds_default_headers() {
877        let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
878            .await
879            .expect("bind");
880        let addr = listener.local_addr().expect("addr");
881        let client_task = tokio::spawn(async move {
882            tokio::net::TcpStream::connect(addr).await.expect("connect")
883        });
884        let (mut server_stream, _) =
885            listener.accept().await.expect("accept");
886        let mut client = client_task.await.expect("join");
887
888        let response = Response::new(200, "OK", b"hello".to_vec());
889        send_response_async(&mut server_stream, &response)
890            .await
891            .expect("send");
892        drop(server_stream);
893
894        let mut bytes = Vec::new();
895        let _ = client.read_to_end(&mut bytes).await.expect("read");
896        let text = String::from_utf8(bytes).expect("utf8");
897        assert!(text.contains("HTTP/1.1 200 OK"));
898        assert!(text.contains("Content-Length: 5"));
899        assert!(text.contains("Connection: close"));
900        assert!(text.ends_with("hello"));
901    }
902
903    #[tokio::test(flavor = "current_thread")]
904    async fn send_response_async_keeps_existing_headers() {
905        let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
906            .await
907            .expect("bind");
908        let addr = listener.local_addr().expect("addr");
909        let client_task = tokio::spawn(async move {
910            tokio::net::TcpStream::connect(addr).await.expect("connect")
911        });
912        let (mut server_stream, _) =
913            listener.accept().await.expect("accept");
914        let mut client = client_task.await.expect("join");
915
916        let mut response = Response::new(204, "No Content", Vec::new());
917        response.headers.push(("Content-Length".into(), "0".into()));
918        response
919            .headers
920            .push(("Connection".into(), "keep-alive".into()));
921        send_response_async(&mut server_stream, &response)
922            .await
923            .expect("send");
924        drop(server_stream);
925
926        let mut bytes = Vec::new();
927        let _ = client.read_to_end(&mut bytes).await.expect("read");
928        let text = String::from_utf8(bytes).expect("utf8");
929        assert!(text.contains("Content-Length: 0"));
930        assert!(text.contains("Connection: keep-alive"));
931        assert!(!text.contains("Connection: close"));
932    }
933
934    #[tokio::test(flavor = "current_thread")]
935    async fn handle_async_connection_rejects_invalid_utf8() {
936        let dir = tempfile::tempdir().expect("tempdir");
937        let server = Server::builder()
938            .address("127.0.0.1:0")
939            .document_root(dir.path().to_string_lossy().as_ref())
940            .build()
941            .expect("server");
942
943        let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
944            .await
945            .expect("bind");
946        let addr = listener.local_addr().expect("addr");
947        let client_task = tokio::spawn(async move {
948            let mut stream = tokio::net::TcpStream::connect(addr)
949                .await
950                .expect("connect");
951            stream.write_all(b"\xFF\xFE").await.expect("write");
952            stream
953        });
954        let (server_stream, _) =
955            listener.accept().await.expect("accept");
956        let _client = client_task.await.expect("join");
957
958        let err = handle_async_connection(
959            server_stream,
960            &server,
961            &PerfLimits::default(),
962        )
963        .await
964        .expect_err("invalid utf8 should fail");
965        assert!(err.to_string().contains("Invalid request"));
966    }
967
968    #[tokio::test(flavor = "current_thread")]
969    async fn handle_async_connection_returns_ok_on_clean_close() {
970        let dir = tempfile::tempdir().expect("tempdir");
971        let server = Server::builder()
972            .address("127.0.0.1:0")
973            .document_root(dir.path().to_string_lossy().as_ref())
974            .build()
975            .expect("server");
976
977        let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
978            .await
979            .expect("bind");
980        let addr = listener.local_addr().expect("addr");
981        let client_task = tokio::spawn(async move {
982            let stream = tokio::net::TcpStream::connect(addr)
983                .await
984                .expect("connect");
985            drop(stream);
986        });
987        let (server_stream, _) =
988            listener.accept().await.expect("accept");
989        client_task.await.expect("join");
990
991        handle_async_connection(
992            server_stream,
993            &server,
994            &PerfLimits::default(),
995        )
996        .await
997        .expect("clean close");
998    }
999
1000    #[tokio::test(flavor = "current_thread")]
1001    async fn handle_async_connection_sends_built_response() {
1002        let dir = tempfile::tempdir().expect("tempdir");
1003        let root = dir.path();
1004        std::fs::create_dir(root.join("404")).expect("404 dir");
1005        std::fs::write(root.join("404/index.html"), "not found")
1006            .expect("404");
1007        let server = Server::builder()
1008            .address("127.0.0.1:0")
1009            .document_root(root.to_string_lossy().as_ref())
1010            .build()
1011            .expect("server");
1012
1013        let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
1014            .await
1015            .expect("bind");
1016        let addr = listener.local_addr().expect("addr");
1017        let client_task = tokio::spawn(async move {
1018            let mut stream = tokio::net::TcpStream::connect(addr)
1019                .await
1020                .expect("connect");
1021            stream
1022                .write_all(
1023                    b"GET /missing.txt HTTP/1.1\r\nHost: localhost\r\n\r\n",
1024                )
1025                .await
1026                .expect("write");
1027            stream
1028        });
1029        let (server_stream, _) =
1030            listener.accept().await.expect("accept");
1031        let mut client = client_task.await.expect("join");
1032        handle_async_connection(
1033            server_stream,
1034            &server,
1035            &PerfLimits::default(),
1036        )
1037        .await
1038        .expect("handled");
1039
1040        let mut bytes = Vec::new();
1041        let _ = client.read_to_end(&mut bytes).await.expect("read");
1042        let text = String::from_utf8(bytes).expect("utf8");
1043        assert!(text.contains("HTTP/1.1"));
1044    }
1045
1046    #[tokio::test(flavor = "current_thread")]
1047    async fn fast_path_includes_precompressed_encoding_headers() {
1048        let dir = tempfile::tempdir().expect("tempdir");
1049        let root = dir.path();
1050        std::fs::write(root.join("index.html"), "plain").expect("base");
1051        std::fs::write(root.join("index.html.gz"), "gzdata")
1052            .expect("gz");
1053        let server = Server::builder()
1054            .address("127.0.0.1:0")
1055            .document_root(root.to_string_lossy().as_ref())
1056            .build()
1057            .expect("server");
1058
1059        let mut headers = HashMap::new();
1060        let _ = headers
1061            .insert("accept-encoding".to_string(), "gzip".to_string());
1062        let req = Request {
1063            method: "GET".into(),
1064            path: "/index.html".into(),
1065            version: "HTTP/1.1".into(),
1066            headers,
1067        };
1068
1069        let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
1070            .await
1071            .expect("bind");
1072        let addr = listener.local_addr().expect("addr");
1073        let client_task = tokio::spawn(async move {
1074            tokio::net::TcpStream::connect(addr).await.expect("connect")
1075        });
1076        let (mut server_stream, _) =
1077            listener.accept().await.expect("accept");
1078        let mut client = client_task.await.expect("join");
1079
1080        assert!(
1081            try_send_static_file_fast_path(
1082                &mut server_stream,
1083                &server,
1084                &req,
1085                u64::MAX
1086            )
1087            .await
1088            .expect("served")
1089        );
1090        drop(server_stream);
1091        let mut bytes = Vec::new();
1092        let _ = client.read_to_end(&mut bytes).await.expect("read");
1093        let text = String::from_utf8(bytes).expect("utf8");
1094        assert!(text.contains("Content-Encoding: gzip"));
1095        assert!(text.contains("Vary: Accept-Encoding"));
1096    }
1097
1098    #[test]
1099    fn resolve_static_path_handles_missing_dir_index_and_immutable_edge_cases()
1100     {
1101        let dir = tempfile::tempdir().expect("tempdir");
1102        let root = dir.path();
1103        std::fs::create_dir(root.join("dir-no-index")).expect("mkdir");
1104        assert!(resolve_static_path(root, "/dir-no-index").is_none());
1105        assert!(!is_probably_immutable_asset("/assets/noext"));
1106        assert!(!is_probably_immutable_asset("/assets/file.js"));
1107    }
1108
1109    #[tokio::test(flavor = "current_thread")]
1110    async fn try_send_static_file_fast_path_missing_file_returns_false()
1111    {
1112        let dir = tempfile::tempdir().expect("tempdir");
1113        let server = Server::builder()
1114            .address("127.0.0.1:0")
1115            .document_root(dir.path().to_string_lossy().as_ref())
1116            .build()
1117            .expect("server");
1118        let request = Request {
1119            method: "GET".into(),
1120            path: "/missing.txt".into(),
1121            version: "HTTP/1.1".into(),
1122            headers: HashMap::new(),
1123        };
1124
1125        let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
1126            .await
1127            .expect("bind");
1128        let addr = listener.local_addr().expect("addr");
1129        let client_task = tokio::spawn(async move {
1130            tokio::net::TcpStream::connect(addr).await.expect("connect")
1131        });
1132        let (mut server_stream, _) =
1133            listener.accept().await.expect("accept");
1134        let _client = client_task.await.expect("join");
1135
1136        let served = try_send_static_file_fast_path(
1137            &mut server_stream,
1138            &server,
1139            &request,
1140            u64::MAX,
1141        )
1142        .await
1143        .expect("missing file should map to false");
1144        assert!(!served);
1145    }
1146
1147    #[cfg(any(target_os = "linux", target_os = "android"))]
1148    #[tokio::test(flavor = "current_thread")]
1149    async fn try_sendfile_unix_sends_file_bytes() {
1150        let dir = tempfile::tempdir().expect("tempdir");
1151        let path = dir.path().join("blob.bin");
1152        let payload = b"abcdef123456";
1153        std::fs::write(&path, payload).expect("write");
1154
1155        let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
1156            .await
1157            .expect("bind");
1158        let addr = listener.local_addr().expect("addr");
1159        let client_task = tokio::spawn(async move {
1160            tokio::net::TcpStream::connect(addr).await.expect("connect")
1161        });
1162        let (server_stream, _) =
1163            listener.accept().await.expect("accept");
1164        let mut client = client_task.await.expect("join");
1165
1166        let sent = try_sendfile_unix(
1167            &server_stream,
1168            &path,
1169            payload.len() as u64,
1170        )
1171        .await
1172        .expect("sendfile");
1173        assert!(sent);
1174        drop(server_stream);
1175
1176        let mut got = Vec::new();
1177        let _ = client.read_to_end(&mut got).await.expect("read");
1178        assert_eq!(got, payload);
1179    }
1180
1181    #[tokio::test(flavor = "current_thread")]
1182    async fn start_high_perf_accepts_and_serves_then_can_abort() {
1183        let dir = tempfile::tempdir().expect("tempdir");
1184        std::fs::write(dir.path().join("index.html"), "ok")
1185            .expect("write");
1186
1187        let probe = std::net::TcpListener::bind("127.0.0.1:0")
1188            .expect("probe bind");
1189        let addr = probe.local_addr().expect("probe addr");
1190        drop(probe);
1191
1192        let server = Server::builder()
1193            .address(&addr.to_string())
1194            .document_root(dir.path().to_string_lossy().as_ref())
1195            .build()
1196            .expect("server");
1197        let limits = PerfLimits {
1198            max_inflight: 1,
1199            max_queue: 1,
1200            sendfile_threshold_bytes: u64::MAX,
1201        };
1202
1203        let task = tokio::spawn(async move {
1204            let _ = start_high_perf(server, limits).await;
1205        });
1206
1207        tokio::time::sleep(Duration::from_millis(50)).await;
1208        let mut client = tokio::net::TcpStream::connect(addr)
1209            .await
1210            .expect("connect");
1211        client
1212            .write_all(
1213                b"GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n",
1214            )
1215            .await
1216            .expect("write");
1217        let mut buf = vec![0_u8; 512];
1218        let read =
1219            timeout(Duration::from_secs(1), client.read(&mut buf))
1220                .await
1221                .expect("timed read")
1222                .expect("read");
1223        assert!(read > 0);
1224        let text = String::from_utf8_lossy(&buf[..read]);
1225        assert!(text.contains("HTTP/1.1 200 OK"));
1226
1227        task.abort();
1228        let join = task.await;
1229        assert!(join.is_err());
1230    }
1231}