Project homepage Mailing List  Warmcat.com  API Docs  Github Mirror 
    npro  
 Modern all-safe Rust Network Protocol library supporting h1, h2, h3, ws, wt sans-IO and with socket IO + tls
git clone https://npro.rs/repo/npro
 
root / crates / npro-test / h1 / requests / headers-cr-cr.http
Author[]Andy Green <andy@warmcat.com> 2026-10-05 10:34 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-10-05 13:01 UTC
Treeb100125622320814946fae0841902f4069a66aed   Raw Patch
 
npro-test: npro's h1 server replays C's server transcripts
npro-test: npro's h1 server replays C's server transcripts

19 of C's server transcripts, each one connection to C's sansio-uri vhost
(a GET's target limited to 33 bytes, a User-Agent to 16), replayed
through npro_h1::server with C's app ported as api-test-sansio has it:
200 text/plain with the path, a newline and the urlargs; /short with a
length of 10 and 3 bytes, none to a HEAD; /body-done from the body's
first piece.  What the server writes after each rx must be the
transcript's tx bytes, and is: the h1-uri-* and h1-reqline-* cases,
h1-header-past-limit, h1-post-no-length (a POST with no length, and the
GET pipelined behind it), h1-short-answer and h1-body-done.

A reason phrase planted wrong fails it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019kg5Eemy68ZaqDBcUJQG6J
diff --git a/crates/npro-test/tests/h1_server_replay.rs b/crates/npro-test/tests/h1_server_replay.rs new file mode 100644 index 0000000..28129a5 --- /dev/null +++ b/crates/npro-test/tests/h1_server_replay.rs @@ -0,0 +1,194 @@ +//! npro's h1 server replays C's server transcripts byte for byte. +//! +//! Each transcript here is one connection to C's `sansio-uri` vhost, whose +//! context limits a GET's target to 33 bytes and a User-Agent to 16, and +//! whose app is ported below as C's `api-test-sansio` has it: it answers +//! 200 `text/plain` with the path, a newline and the urlargs; `/short` +//! with a length of 10 and 3 bytes (none to a HEAD); `/body-done` from the +//! body's first piece, with `ok\n`. Each `rx` is handed to npro's server, +//! and what it writes before the next must be the transcript's `tx` bytes. + +#![expect( + unused_crate_dependencies, + reason = "an integration test sees all of its crate's dependencies; this one uses npro-test and npro-h1" +)] + +// held to clippy's rules for tests +#[cfg(test)] +mod h1_server_replay { + use core::num::NonZeroU16; + + use npro_h1::head; + use npro_h1::server::{Config, Event, Response, Server, TxSource}; + use npro_h1::table::DEFAULT_CAPACITY; + use npro_h1::token::Token; + use npro_test::{StepKind, Transcript, vendored}; + + /// The cases this replays: every server transcript whose connection is + /// h1 to `sansio-uri` and needs no more of lws than this phase has. + const CASES: [&str; 19] = [ + "h1-uri-dotdot-args", + "h1-uri-dot-args", + "h1-uri-plus", + "h1-uri-at-limit", + "h1-uri-past-limit", + "h1-header-past-limit", + "h1-reqline-http09", + "h1-reqline-no-method", + "h1-reqline-version-2", + "h1-reqline-version-junk", + "h1-reqline-version-long", + "h1-reqline-version-1-2", + "h1-reqline-unknown-method", + "h1-reqline-unknown-header-first", + "h1-reqline-leading-empty", + "h1-reqline-leading-empty-many", + "h1-post-no-length", + "h1-short-answer", + "h1-body-done", + ]; + + /// C's `callback_uri`, for what these cases ask of it. + #[derive(Default)] + struct UriApp { + /// The payload still to go. + out: Vec<u8>, + at: usize, + /// Complete the transaction once the payload has gone. + complete_when_sent: bool, + /// `/body-done`: answer from the body. + body_done: bool, + } + + impl TxSource for UriApp { + fn fill(&mut self, buf: &mut [u8]) -> usize { + let rest = &self.out[self.at..]; + let n = rest.len().min(buf.len()); + buf[..n].copy_from_slice(&rest[..n]); + self.at = self.at.checked_add(n).unwrap(); + n + } + } + + impl UriApp { + fn answer(&mut self, s: &mut Server<Vec<u8>>, len: u64, payload: &[u8]) { + s.respond(Response { + status: 200, + content_type: Some(b"text/plain"), + content_length: Some(len), + }) + .unwrap(); + self.out = payload.to_vec(); + self.at = 0; + self.complete_when_sent = true; + } + + fn request(&mut self, s: &mut Server<Vec<u8>>) { + let t = s.request(); + let path = t + .first(Token::GetUri) + .or_else(|| t.first(Token::PostUri)) + .or_else(|| t.first(Token::HeadUri)) + .unwrap_or_default() + .to_vec(); + let head = t.is_present(Token::HeadUri); + let mut args = vec![0u8; t.total_len(Token::UriArgs)]; + t.copy(Token::UriArgs, &mut args).unwrap(); + match path.as_slice() { + b"/short" => self.answer(s, 10, if head { b"" } else { b"abc" }), + b"/body-done" => self.body_done = true, + _ => { + let body = [path, b"\n".to_vec(), args].concat(); + let len = u64::try_from(body.len()).unwrap(); + self.answer(s, len, &body); + } + } + } + + fn body(&mut self, s: &mut Server<Vec<u8>>) { + if self.body_done { + self.body_done = false; + self.answer(s, 3, b"ok\n"); + } + } + } + + /// Hands `input` to the server, with the app answering, until neither + /// takes or writes anything more; returns what was written. + fn feed(s: &mut Server<Vec<u8>>, app: &mut UriApp, mut input: &[u8]) -> Vec<u8> { + let mut wrote = Vec::new(); + let mut buf = [0u8; 1024]; + loop { + let rx = s.rx(input); + let mut progress = rx.consumed > 0 || rx.event.is_some(); + input = &input[rx.consumed..]; + match rx.event { + Some(Event::Request) => app.request(s), + Some(Event::Body(_)) => app.body(s), + Some(Event::BodyEnd) | None => {} + } + loop { + let tx = s.tx(&mut buf, app); + wrote.extend_from_slice(&buf[..tx.written]); + if app.complete_when_sent && app.at == app.out.len() && !s.wants_write() { + app.complete_when_sent = false; + s.complete(); + progress = true; + } + if tx.written == 0 { + break; + } + progress = true; + } + if !progress { + return wrote; + } + } + } + + fn replay(t: &Transcript) { + let cfg = head::Config::new() + .with_limit(Token::GetUri, NonZeroU16::new(33).unwrap()) + .with_limit(Token::UserAgent, NonZeroU16::new(16).unwrap()); + let mut s = Server::new(vec![0u8; DEFAULT_CAPACITY], Config::new(cfg)).unwrap(); + let mut app = UriApp::default(); + let mut steps = t.steps.iter().peekable(); + while let Some(step) = steps.next() { + let StepKind::Rx(rx) = &step.kind else { + panic!("{}: {:?} with no rx before it", t.case, step.kind); + }; + let mut want = Vec::new(); + while let Some(next) = steps.peek() { + match &next.kind { + StepKind::Tx(b) => want.extend_from_slice(b), + StepKind::Rx(_) => break, + k @ (StepKind::AppRx(_) | StepKind::Close) => { + panic!("{}: {k:?} is not in this replay", t.case) + } + } + steps.next(); + } + let got = feed(&mut s, &mut app, rx); + assert_eq!( + got.escape_ascii().to_string(), + want.escape_ascii().to_string(), + "{} at {}us", + t.case, + step.t_us + ); + } + } + + #[test] + #[cfg_attr(miri, ignore = "reads the transcripts: native runs keep it")] + fn cs_server_transcripts_replay_byte_for_byte() { + let all = vendored().unwrap(); + for case in CASES { + let t = all + .iter() + .find(|t| t.case == case) + .unwrap_or_else(|| panic!("no transcript {case}")); + replay(t); + } + } +}
Page fetched 0s ago, creation time: 6ms (vhost etag hits: 0%, cache hits: 0%)