1#[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#[cfg(feature = "high-perf")]
44#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
45#[derive(Clone, Copy, Debug)]
58pub struct PerfLimits {
59 pub max_inflight: usize,
61 pub max_queue: usize,
63 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#[cfg(feature = "high-perf")]
84#[cfg_attr(docsrs, doc(cfg(feature = "high-perf")))]
85pub 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}