From 054c72a415ed9fd689430f8ce6781c14414f44d2 Mon Sep 17 00:00:00 2001 From: Charles Sibbald Date: Sat, 18 Apr 2026 00:25:58 +0300 Subject: [PATCH 1/6] feat(response): allow owned header values (no Box::leak) --- src/lib.rs | 2 +- src/response.rs | 196 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 191 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d444ff1..4e558ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,4 +10,4 @@ pub use http_server::{HttpServer, HttpServerWithHeaders, HttpService, HttpServic pub use request::{ decode_default, decode_large, decode_standard, decode_xlarge, BodyReader, MaxHeaders, Request, }; -pub use response::Response; +pub use response::{IntoResponseHeader, Response, ResponseHeader}; diff --git a/src/response.rs b/src/response.rs index 7dab965..c9b9898 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,10 +1,110 @@ +use std::borrow::Cow; use std::io; use crate::request::MAX_HEADERS; use bytes::BytesMut; + +/// A single HTTP response header value. +/// +/// This type lets callers pass either `&'static str` (the previous fast path, +/// zero allocation) or an owned string (`String` / `Box` / `Cow::Owned`) +/// for headers whose value is computed per-response (for example a generated +/// request id, a dynamic `Content-Type`, or an `Accept-Post` list built from +/// OpenAPI metadata). +/// +/// Owned variants are dropped when the [`Response`] is dropped, so using +/// owned values here does **not** leak memory — unlike the previous workaround +/// that required callers to `Box::leak` their formatted header strings to +/// satisfy the `&'static str` requirement on [`Response::header`]. +/// +/// In the common static case the `Static` variant stores the same fat pointer +/// as a `&'static str`, so there is no additional indirection on the encode +/// hot path. +#[derive(Debug)] +pub enum ResponseHeader { + /// A header whose value has `'static` lifetime (e.g. a string literal). + /// Matches the original `may_minihttp` behavior — no allocation, no drop. + Static(&'static str), + /// A header whose value is owned by this response. + /// Freed when the response is dropped. + Owned(Box), +} + +impl ResponseHeader { + /// Borrow the header line as `&str`. + #[inline] + pub fn as_str(&self) -> &str { + match self { + ResponseHeader::Static(s) => s, + ResponseHeader::Owned(s) => s, + } + } + + /// Borrow the header line as raw bytes (what `encode` writes). + #[inline] + pub fn as_bytes(&self) -> &[u8] { + self.as_str().as_bytes() + } +} + +impl Default for ResponseHeader { + #[inline] + fn default() -> Self { + ResponseHeader::Static("") + } +} + +/// Conversion trait that lets [`Response::header`] accept either static or +/// owned string-like values without an API split. +/// +/// Implementations are provided for `&'static str`, `String`, `Box`, and +/// `Cow<'static, str>`. All of them are zero-cost in the `Static` case and +/// perform a single `into_boxed_str()` / no-op in the owned case. +pub trait IntoResponseHeader { + fn into_response_header(self) -> ResponseHeader; +} + +impl IntoResponseHeader for &'static str { + #[inline] + fn into_response_header(self) -> ResponseHeader { + ResponseHeader::Static(self) + } +} + +impl IntoResponseHeader for String { + #[inline] + fn into_response_header(self) -> ResponseHeader { + ResponseHeader::Owned(self.into_boxed_str()) + } +} + +impl IntoResponseHeader for Box { + #[inline] + fn into_response_header(self) -> ResponseHeader { + ResponseHeader::Owned(self) + } +} + +impl IntoResponseHeader for Cow<'static, str> { + #[inline] + fn into_response_header(self) -> ResponseHeader { + match self { + Cow::Borrowed(s) => ResponseHeader::Static(s), + Cow::Owned(s) => ResponseHeader::Owned(s.into_boxed_str()), + } + } +} + +impl IntoResponseHeader for ResponseHeader { + #[inline] + fn into_response_header(self) -> ResponseHeader { + self + } +} + pub struct Response<'a> { - headers: [&'static str; MAX_HEADERS], + headers: [ResponseHeader; MAX_HEADERS], headers_len: usize, status_message: StatusMessage, body: Body, @@ -24,10 +124,8 @@ struct StatusMessage { impl<'a> Response<'a> { pub(crate) fn new(rsp_buf: &'a mut BytesMut) -> Response<'a> { - let headers: [&'static str; 16] = [""; 16]; - Response { - headers, + headers: std::array::from_fn(|_| ResponseHeader::Static("")), headers_len: 0, body: Body::Dummy, status_message: StatusMessage { @@ -44,9 +142,28 @@ impl<'a> Response<'a> { self } + /// Append a header line to the response. + /// + /// Accepts both `&'static str` (zero allocation, identical to the previous + /// behavior) and owned strings (`String`, `Box`, `Cow<'static, str>`). + /// Owned header values are freed when the response is dropped — there is + /// no need for callers to `Box::leak` formatted values. + /// + /// ``` + /// # use may_minihttp::{Response, ResponseHeader}; + /// # use bytes::BytesMut; + /// # let mut buf = BytesMut::new(); + /// # let mut res = Response::_test_new(&mut buf); + /// // static fast path (no allocation) + /// res.header("Content-Type: application/json"); + /// + /// // owned path (freed with the response) + /// let request_id = format!("X-Request-ID: {}", "01J…"); + /// res.header(request_id); + /// ``` #[inline] - pub fn header(&mut self, header: &'static str) -> &mut Self { - self.headers[self.headers_len] = header; + pub fn header(&mut self, header: H) -> &mut Self { + self.headers[self.headers_len] = header.into_response_header(); self.headers_len += 1; self } @@ -94,6 +211,13 @@ impl<'a> Response<'a> { Body::Vec(ref v) => v, } } + + /// Test-only constructor used by the doc example above. Not part of the + /// public API; gated so downstream crates cannot accidentally rely on it. + #[doc(hidden)] + pub fn _test_new(rsp_buf: &'a mut BytesMut) -> Response<'a> { + Response::new(rsp_buf) + } } impl Drop for Response<'_> { @@ -144,3 +268,63 @@ pub(crate) fn encode_error(e: io::Error, buf: &mut BytesMut) { buf.extend_from_slice(b"\r\n\r\n"); buf.extend_from_slice(msg); } + +#[cfg(test)] +mod tests { + use super::*; + use std::borrow::Cow; + + /// A `&'static str` header takes the Static fast path and stores the + /// exact same slice. No allocation. + #[test] + fn header_static_is_zero_alloc() { + let hdr: ResponseHeader = "Content-Type: text/plain".into_response_header(); + match hdr { + ResponseHeader::Static(s) => assert_eq!(s, "Content-Type: text/plain"), + ResponseHeader::Owned(_) => panic!("static input must take Static variant"), + } + } + + /// `String` / `Box` / `Cow::Owned` take the Owned variant and are + /// freed with the response; no `Box::leak` needed by callers. + #[test] + fn header_owned_variants_are_accepted() { + let s: ResponseHeader = String::from("X-Req-Id: 01J").into_response_header(); + assert!(matches!(s, ResponseHeader::Owned(_))); + + let b: ResponseHeader = Box::::from("X-Foo: bar").into_response_header(); + assert!(matches!(b, ResponseHeader::Owned(_))); + + let c_borrowed: ResponseHeader = + Cow::<'static, str>::Borrowed("Content-Type: application/json").into_response_header(); + assert!(matches!(c_borrowed, ResponseHeader::Static(_))); + + let c_owned: ResponseHeader = + Cow::<'static, str>::Owned(String::from("X-Trace: abc")).into_response_header(); + assert!(matches!(c_owned, ResponseHeader::Owned(_))); + } + + /// `Response::header()` accepts both static and owned headers without an + /// API split, and `encode()` writes both correctly to the response buffer. + #[test] + fn encode_mixes_static_and_owned_headers() { + let mut rsp_buf = BytesMut::new(); + let mut out = BytesMut::new(); + { + let mut res = Response::new(&mut rsp_buf); + res.status_code(200, "OK"); + res.header("Content-Type: application/json"); + res.header(format!("X-Request-ID: {}", "01J")); + res.header(Cow::<'static, str>::Owned(String::from("X-Trace: abc"))); + res.body("ok"); + // `encode` consumes the response via `mut rsp: Response` + encode(res, &mut out); + } + let response_str = std::str::from_utf8(&out).expect("utf8"); + assert!(response_str.starts_with("HTTP/1.1 200 Ok\r\n")); + assert!(response_str.contains("\r\nContent-Type: application/json\r\n")); + assert!(response_str.contains("\r\nX-Request-ID: 01J\r\n")); + assert!(response_str.contains("\r\nX-Trace: abc\r\n")); + assert!(response_str.ends_with("\r\n\r\nok")); + } +} From 580447bf909cbcbf08b5bd8aac95ab7cd30b3946 Mon Sep 17 00:00:00 2001 From: Charles Sibbald Date: Sat, 18 Apr 2026 12:06:38 +0300 Subject: [PATCH 2/6] ci: fix clippy + Dockerfile Rust version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - examples/techempower.rs: sort_by → sort_by_key (clippy unnecessary_sort_by) - examples/headers.rs: inline {bind_addr} (clippy uninlined_format_args) - docker/Dockerfile.test: rust 1.83 → 1.85 so transitive cpufeatures v0.3 (edition2024) can build --- docker/Dockerfile.test | 5 ++++- examples/headers.rs | 2 +- examples/techempower.rs | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile.test b/docker/Dockerfile.test index 37063ed..536290d 100644 --- a/docker/Dockerfile.test +++ b/docker/Dockerfile.test @@ -1,4 +1,7 @@ -FROM rust:1.83-slim AS builder +# Rust 1.85 is required: transitive deps like `cpufeatures` (pulled in via +# `sha2` / OpenSSL paths) need the `edition2024` feature, which Cargo +# stabilised in 1.85 (Feb 2025). Rust 1.83 fails to parse those manifests. +FROM rust:1.85-slim AS builder WORKDIR /app # Install build dependencies for OpenSSL and other native dependencies diff --git a/examples/headers.rs b/examples/headers.rs index 244ac42..4a2add7 100644 --- a/examples/headers.rs +++ b/examples/headers.rs @@ -36,7 +36,7 @@ fn main() { .start(&bind_addr) .unwrap(); - println!("Server listening on http://{}", bind_addr); + println!("Server listening on http://{bind_addr}"); println!("Configured to accept up to 32 headers"); server.wait(); } diff --git a/examples/techempower.rs b/examples/techempower.rs index 6d9738c..19762fe 100644 --- a/examples/techempower.rs +++ b/examples/techempower.rs @@ -71,7 +71,7 @@ mod __impl { .map(|_| may::go!(move || PgConnection::new(db_url))) .collect::>(); let mut clients: Vec<_> = clients.into_iter().map(|t| t.join().unwrap()).collect(); - clients.sort_by(|a, b| (a.client.id() % size).cmp(&(b.client.id() % size))); + clients.sort_by_key(|c| c.client.id() % size); // for c in &clients { // println!("client id: {}", c.client.id()); // } From 8d82724ca985e37012ea5b25558a15776dcd9504 Mon Sep 17 00:00:00 2001 From: Charles Sibbald Date: Sat, 18 Apr 2026 12:10:32 +0300 Subject: [PATCH 3/6] ci: bump Dockerfile.test to Rust 1.88 for cookie_store/time/icu deps --- docker/Dockerfile.test | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docker/Dockerfile.test b/docker/Dockerfile.test index 536290d..62dffaf 100644 --- a/docker/Dockerfile.test +++ b/docker/Dockerfile.test @@ -1,7 +1,10 @@ -# Rust 1.85 is required: transitive deps like `cpufeatures` (pulled in via -# `sha2` / OpenSSL paths) need the `edition2024` feature, which Cargo -# stabilised in 1.85 (Feb 2025). Rust 1.83 fails to parse those manifests. -FROM rust:1.85-slim AS builder +# Rust 1.88 is the current minimum for our transitive dep tree: +# * `cpufeatures` / `edition2024` → needs ≥ 1.85 (stabilised Feb 2025) +# * `icu_*` (pulled via `reqwest`/TLS) → needs ≥ 1.86 +# * `cookie_store`, `time`, `time-macros` → need ≥ 1.88 +# Bumping to a single explicit tag keeps the build reproducible; when deps +# push past this again, bump in lockstep rather than floating on `rust:slim`. +FROM rust:1.88-slim AS builder WORKDIR /app # Install build dependencies for OpenSSL and other native dependencies From 826a761933c53c7927d55e92443691065dcc5b7e Mon Sep 17 00:00:00 2001 From: Charles Sibbald <123247+casibbald@users.noreply.github.com> Date: Wed, 6 May 2026 10:33:07 +0300 Subject: [PATCH 4/6] Fix/remove redundant preparse check (#25) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(response): allow owned header values (no Box::leak) * ci: fix clippy + Dockerfile Rust version - examples/techempower.rs: sort_by → sort_by_key (clippy unnecessary_sort_by) - examples/headers.rs: inline {bind_addr} (clippy uninlined_format_args) - docker/Dockerfile.test: rust 1.83 → 1.85 so transitive cpufeatures v0.3 (edition2024) can build * ci: bump Dockerfile.test to Rust 1.88 for cookie_store/time/icu deps * perf(request): remove redundant \r\n\r\n pre-scan before httparse The pre-scan at the top of decode() was added to fix issue #18 ("Token" parsing errors when headers arrive across multiple TCP packets) by returning Ok(None) whenever the buffer didn't yet contain the end-of- headers marker. The pre-check is redundant because httparse::Request::parse already returns Status::Partial on every form of truncation we care about: - mid-method, mid-path, mid-version - mid-header-name, mid-header-value - buffer ending before the terminating \r\n\r\n Our Partial arm below already maps that to Ok(None), so the pre-check only adds an O(n) window scan on every decode() call — one that's particularly costly in the warm read path where the buffer is typically a full 8 KB frame. The "Token" errors from issue #18 are unrelated to the end-of-headers marker: httparse only returns Token / HeaderName / Version on genuinely malformed bytes (e.g. 'G@T', space in path). Those are permanent failures regardless of how much more data arrives; surfacing them as 400 at first sight is more correct than stalling the connection until a \r\n\r\n happens to show up later. An empirical probe across 15 input shapes (each truncation + each malformed case) confirms: truncation always yields Partial, malformed bytes always yield their specific error class. No scenario existed where the pre-check converted a Token error into a Partial. Small hot-path win (saves one linear scan per request) with no behavioural change on valid or invalid traffic. * test(request_parsing): ignore wall-clock perf smokes + assert correctness The two tests in the `performance` sub-module (`benchmark_header_counting`, `benchmark_completion_check`) assert on absolute wall-clock thresholds (100 ms / 2 s) against test-local helpers (`count_headers`, `has_complete_headers`). That combination is fundamentally CI-hostile: - Debug-build timing varies by an order of magnitude under concurrent test load. On this machine `benchmark_completion_check` runs in ~0.8 s in isolation and ~2.0 s when the whole suite runs in parallel — straddling the 2 s bound and flaking either way depending on scheduler luck. - They benchmark *test-local helpers*, not production parser code. A regression here would signal the test harness got slower, not that `decode()` or `httparse` did. Fix: - Mark both `#[ignore]` so `cargo test` skips them by default while `cargo test -- --ignored performance::` still runs them for manual perf spot-checks. - Replace the fragile `assert!(duration < X)` with stable correctness assertions (every iteration returns the same expected count / hit), guarded by `std::hint::black_box` to prevent the optimiser from turning the loop into a no-op. - Add a module-level doc comment explaining the intent and the --ignored invocation. Resulting test counts: - default: 19 passed / 2 ignored (was 21 passed, occasionally 1 FAIL) - ignored: 2 passed / 0 failed (runs in ~0.5 s) --- src/request.rs | 22 +++++++++++++++------- tests/request_parsing.rs | 40 ++++++++++++++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/request.rs b/src/request.rs index 59778de..e1964ae 100644 --- a/src/request.rs +++ b/src/request.rs @@ -305,13 +305,21 @@ pub fn decode<'header, 'buf, 'stream, const N: usize>( // so we can transfer the mutable reference to Request let buf: &[u8] = unsafe { std::mem::transmute(req_buf.chunk()) }; - // Wait for complete headers before parsing to prevent token errors - // This fixes issue #18 where headers arriving in multiple TCP packets - // would cause "Token" parsing errors - // The \r\n\r\n sequence marks the end of HTTP headers - if !buf.windows(4).any(|window| window == b"\r\n\r\n") { - return Ok(None); // Need more data - } + // Note: no `\r\n\r\n` pre-scan — `httparse::Request::parse` already + // returns `Status::Partial` on any truncated input (mid-method, + // mid-path, mid-version, mid-header-name, mid-header-value, or just + // missing the terminating `\r\n\r\n`). The `Status::Partial` arm below + // maps that to `Ok(None)`, which is identical to what the old + // `buf.windows(4).any(|w| w == b"\r\n\r\n")` pre-check did — only + // without an O(n) re-scan of the buffer before the real parse. + // + // An empirical probe across 15 input shapes (see PR description) + // confirmed: every truncation yields `Partial`, never `Token`. The + // `Token` / `HeaderName` / `Version` errors only fire on genuinely + // malformed bytes (e.g. `G@T`, space in path), which are permanent + // failures regardless of how much more data arrives — surfacing them + // at first sight (400) is more correct than buffering the connection + // until some later `\r\n\r\n` happens to appear. // Get the header limit before parsing (to avoid borrow issues) let header_limit = headers.len(); diff --git a/tests/request_parsing.rs b/tests/request_parsing.rs index 099fd47..3ab4cc6 100644 --- a/tests/request_parsing.rs +++ b/tests/request_parsing.rs @@ -264,37 +264,61 @@ fn create_request_with_n_headers(n: usize) -> Vec { #[cfg(test)] mod performance { + //! Manual perf smoke checks for the test-local `count_headers` / + //! `has_complete_headers` helpers used by the correctness tests above. + //! + //! These are intentionally `#[ignore]`d because: + //! + //! 1. They assert on absolute wall-clock thresholds (100 ms / 2 s) + //! that are fragile on shared CI runners — debug-build timing + //! varies by an order of magnitude under concurrent test load, + //! so the same code path that runs in 0.8 s in isolation + //! routinely hits 2+ s when the whole suite runs in parallel. + //! 2. They benchmark *test-local helpers*, not production parser + //! code — a regression here would mean the test harness got + //! slower, not that `decode()` or `httparse` did. + //! + //! Run manually with: + //! cargo test --test request_parsing -- --ignored performance:: + use super::*; + use std::hint::black_box; use std::time::Instant; #[test] + #[ignore = "wall-clock perf smoke; run with --ignored"] fn benchmark_header_counting() { let request = create_request_with_n_headers(128); let start = Instant::now(); + let mut total = 0usize; for _ in 0..1000 { - let _ = count_headers(&request); + total = total.wrapping_add(black_box(count_headers(&request))); } let duration = start.elapsed(); - println!("Counted headers 1000 times in {:?}", duration); - assert!(duration.as_millis() < 100, "Should be fast"); + println!("Counted headers 1000 times in {duration:?} (sum={total})"); + // Correctness check: every iteration returns the same stable count. + assert_eq!(total, 128 * 1000, "helper must count all 128 headers"); } #[test] + #[ignore = "wall-clock perf smoke; run with --ignored"] fn benchmark_completion_check() { let request = create_request_with_n_headers(128); let start = Instant::now(); + let mut hits = 0u64; for _ in 0..10000 { - let _ = has_complete_headers(&request); + if black_box(has_complete_headers(&request)) { + hits += 1; + } } let duration = start.elapsed(); - println!("Checked completion 10000 times in {:?}", duration); - // Relaxed assertion - performance varies by system - // On debug builds, 500ms for 10k iterations is acceptable - assert!(duration.as_secs() < 2, "Should complete in reasonable time"); + println!("Checked completion 10000 times in {duration:?} (hits={hits})"); + // Correctness check: a well-formed request always has `\r\n\r\n`. + assert_eq!(hits, 10_000, "helper must detect end-of-headers every pass"); } } From 3eadfd77f772a5316ea7264ddba1a4369d6972f7 Mon Sep 17 00:00:00 2001 From: Xudong Huang Date: Thu, 15 Oct 2026 09:43:39 +0000 Subject: [PATCH 5/6] :pencil: use unstable sort for speed --- Cargo.toml | 7 +++---- examples/techempower.rs | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5e3686f..bab2ab5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ version = "0.1.11" edition = "2021" authors = ["Xudong Huang "] repository = "https://github.com/Xudong-Huang/may_minihttp.git" -homepage = "https://github.com/Xudong-Huang/may_minihttp.git" documentation = "https://docs.rs/may_minihttp" description = "Mini http server that implemented on top of [may](https://github.com/Xudong-Huang/may)" readme = "README.md" @@ -23,7 +22,7 @@ once_cell = "1" may = { version = "0.3.46", default-features = false } [dev-dependencies] -atoi = "2" +atoi = "3" num_cpus = "1.0" smallvec = "1.1" env_logger = "0.11" @@ -34,11 +33,11 @@ yarte = { version = "0.15", features = ["bytes-buf", "json"] } buf-min = { version = "0.7", features = ["bytes"] } mimalloc = { version = "0.1", default-features = false } -nanorand = { version = "0.7", default-features = false, features = ["std", "wyrand"] } +nanorand = { version = "0.8", default-features = false, features = ["std", "wyrand"] } # Load testing and integration testing goose = "0.18.1" -testcontainers = "0.15" +testcontainers = "0.28" tokio = { version = "1", features = ["full"] } [target.'cfg(unix)'.dev-dependencies] diff --git a/examples/techempower.rs b/examples/techempower.rs index 19762fe..21ac9b3 100644 --- a/examples/techempower.rs +++ b/examples/techempower.rs @@ -230,7 +230,7 @@ mod __impl { id: 0, message: "Additional fortune added at request time.", }); - fortunes.sort_by(|it, next| it.message.cmp(next.message)); + fortunes.sort_unstable_by(|it, next| it.message.cmp(next.message)); let mut body = unsafe { std::ptr::read(buf) }; ywrite_html!(body, "{{> fortune }}"); From 45d0ac9da9ebb9ccdbb00da58814ec8717a9027a Mon Sep 17 00:00:00 2001 From: Xudong Huang Date: Thu, 15 Oct 2026 09:51:25 +0000 Subject: [PATCH 6/6] :pencil: update WyRand init --- examples/techempower.rs | 2 +- tests/goose_header_load_test.rs | 4 ++-- tests/header_traffic_integration.rs | 4 ++-- tests/test_server.rs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/techempower.rs b/examples/techempower.rs index 21ac9b3..96c8054 100644 --- a/examples/techempower.rs +++ b/examples/techempower.rs @@ -298,7 +298,7 @@ mod __impl { fn new_service(&self, id: usize) -> Self::Service { let db = self.db_pool.get_connection(id); - let rng = WyRand::new(); + let rng = WyRand::new_seed(0); Techempower { db, rng } } } diff --git a/tests/goose_header_load_test.rs b/tests/goose_header_load_test.rs index be060d6..78ebdef 100644 --- a/tests/goose_header_load_test.rs +++ b/tests/goose_header_load_test.rs @@ -195,7 +195,7 @@ impl GooseTestFixture { // - The JoinHandle keeps the server running // - No thread::spawn needed - MAY handles concurrency with coroutines let handle = HttpServer(TestService) - .start(&format!("127.0.0.1:{}", port)) + .start(format!("127.0.0.1:{}", port)) .expect("Failed to start test server"); let fixture = Self { @@ -224,7 +224,7 @@ impl GooseTestFixture { for attempt in 0..max_attempts { if let Ok(mut stream) = StdTcpStream::connect(format!("127.0.0.1:{}", self.port)) { // Send a minimal HTTP request - let request = format!("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"); + let request = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; if stream.write_all(request.as_bytes()).is_ok() { // Try to read some response let mut buf = [0u8; 256]; diff --git a/tests/header_traffic_integration.rs b/tests/header_traffic_integration.rs index 4b9dacd..731328b 100644 --- a/tests/header_traffic_integration.rs +++ b/tests/header_traffic_integration.rs @@ -139,7 +139,7 @@ impl HeaderTestServer { // - The JoinHandle keeps the server running // - No thread::spawn needed - MAY handles concurrency with coroutines let handle = HttpServer(TestService) - .start(&format!("127.0.0.1:{}", port)) + .start(format!("127.0.0.1:{}", port)) .expect("Failed to start test server"); let fixture = Self { @@ -168,7 +168,7 @@ impl HeaderTestServer { for attempt in 0..max_attempts { if let Ok(mut stream) = TcpStream::connect(format!("127.0.0.1:{}", self.port)) { // Send a minimal HTTP request - let request = format!("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"); + let request = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; if stream.write_all(request.as_bytes()).is_ok() { // Try to read some response let mut buf = [0u8; 256]; diff --git a/tests/test_server.rs b/tests/test_server.rs index 699d921..6780d6a 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -34,7 +34,7 @@ impl ConfigurableTestServer { // For now, start with default - we'll add configuration support next let server_handle = thread::spawn(move || { let _server = HttpServer(TestService) - .start(&format!("127.0.0.1:{}", port)) + .start(format!("127.0.0.1:{}", port)) .expect("Failed to start test server"); while !shutdown_clone.load(Ordering::Relaxed) {