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 / reqline-incomplete.http
Author[]Andy Green <andy@warmcat.com> 2026-10-05 16:38 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-10-05 19:18 UTC
Treebb0fc83488b1e189879dba1cf9bfeedb3635c91c   Raw Patch
 
npro-h1: a client's request can ask for an upgrade
npro-h1: a client's request can ask for an upgrade

C's request composer ends the head one of three ways: "connection:
close", nothing for a pipelined connection, or, with do_ws, the lines
asking for the upgrade.  Request's pipeline bool becomes that choice,
Connection::{Close, KeepAlive, Upgrade(lines)}, the lines composed by the
upgraded protocol's crate.

The final response to a request for an upgrade is not framed: whatever
its status, after its head the connection is the upgraded protocol's,
which judges it, as C's lws_client_ws_upgrade() does for ws.  The client
takes nothing more (is_upgraded()), and the bytes after the head are the
caller's to hand on.  An interim response is still dropped first.

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-h1/src/client.rs b/crates/npro-h1/src/client.rs index 8255cdc..20c9fc7 100644 --- a/crates/npro-h1/src/client.rs +++ b/crates/npro-h1/src/client.rs @@ -4,8 +4,9 @@ //! //! [`Client::tx`] writes the request's head, composed as C composes it, in //! C's order: the request line, `Pragma` and `Cache-Control` unless asked -//! not to, `Host`, `Origin`, and `connection: close` unless the connection -//! is to be pipelined. +//! not to, `Host`, `Origin`, then `connection: close`, or nothing for a +//! connection kept for another request, or the header lines asking for an +//! upgrade ([`Connection`]). //! //! [`Client::rx`] takes the response, at most one thing each call: its head //! ([`Event::Response`]), a piece of its body, borrowed from the input @@ -24,6 +25,11 @@ //! //! A response that cannot be framed fails the connection, which asks to be //! released. +//! +//! The final response to a request for an upgrade is not framed at all: +//! after its head, whatever its status, the connection is the upgraded +//! protocol's ([`Client::is_upgraded`]), which judges the response, as C's +//! `lws_client_ws_upgrade()` does for ws. use crate::chunked::{self, Chunk, Dechunk}; use crate::fields::{content_length, transfer_encoding_is_chunked}; @@ -62,9 +68,22 @@ pub struct Request<'a> { /// Send `Pragma: no-cache` and `Cache-Control: no-cache`, unless C's /// `LCCSCF_HTTP_NO_CACHE_CONTROL`. pub no_cache: bool, - /// The connection is kept for another request, rather than the - /// request saying `connection: close`. - pub pipeline: bool, + /// What becomes of the connection after this request. + pub connection: Connection<'a>, +} + +/// What becomes of the connection after the request: the last of the +/// request's headers. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Connection<'a> { + /// It ends with the transaction: `connection: close`. + Close, + /// It is kept for another request, C's pipelining: nothing is said. + KeepAlive, + /// It is upgraded, these header lines, each ending in CRLF, asking for + /// it: C's `do_ws`, whose lines `npro_ws::handshake::ClientKey` + /// composes. + Upgrade(&'a [u8]), } /// What the server's bytes were. @@ -154,14 +173,24 @@ enum Phase { Body(Body), /// The transaction is over. Done, + /// The final response to a request for an upgrade has come: the + /// connection is the upgraded protocol's. + Upgraded, /// The connection failed. Failed(Failure), } +/// What the request asked for: a transaction, or an upgrade. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Asked { + Transaction, + Upgrade, +} + /// One h1 client connection. /// /// ``` -/// use npro_h1::client::{Client, Event, Request, Scheme}; +/// use npro_h1::client::{Client, Connection, Event, Request, Scheme}; /// /// let mut c = Client::new([0u8; 1024], Request { /// method: b"GET", @@ -170,7 +199,7 @@ enum Phase { /// origin: None, /// scheme: Scheme::Http, /// no_cache: false, -/// pipeline: false, +/// connection: Connection::Close, /// })?; /// let mut out = [0u8; 256]; /// let n = c.tx(&mut out); @@ -187,6 +216,7 @@ enum Phase { #[derive(Clone, Debug)] pub struct Client<S> { head: Head<S>, + asked: Asked, phase: Phase, own: Own, status: Option<u16>, @@ -241,6 +271,10 @@ impl<S: AsRef<[u8]> + AsMut<[u8]>> Client<S> { t.snapshot(); Ok(Self { head: Head::with_table(t, Side::Client, head::Config::new()), + asked: match req.connection { + Connection::Close | Connection::KeepAlive => Asked::Transaction, + Connection::Upgrade(_) => Asked::Upgrade, + }, phase: Phase::Asking, own: request_head(&req).map_err(NewError::Head)?, status: None, @@ -270,7 +304,7 @@ impl<S: AsRef<[u8]> + AsMut<[u8]>> Client<S> { pub const fn failed(&self) -> Option<Failure> { match self.phase { Phase::Failed(f) => Some(f), - Phase::Asking | Phase::Head(_) | Phase::Body(_) | Phase::Done => None, + Phase::Asking | Phase::Head(_) | Phase::Body(_) | Phase::Done | Phase::Upgraded => None, } } @@ -280,6 +314,14 @@ impl<S: AsRef<[u8]> + AsMut<[u8]>> Client<S> { matches!(self.phase, Phase::Done) } + /// Whether the final response to a request for an upgrade has come: + /// from the byte after its head, the connection is the upgraded + /// protocol's, and this takes nothing more. + #[must_use] + pub const fn is_upgraded(&self) -> bool { + matches!(self.phase, Phase::Upgraded) + } + /// Writes the request's head, or what is left of it, into `out`, /// returning how much. pub fn tx(&mut self, out: &mut [u8]) -> usize { @@ -308,7 +350,7 @@ impl<S: AsRef<[u8]> + AsMut<[u8]>> Client<S> { }); match self.phase { Phase::Failed(f) => Err(f), - Phase::Asking | Phase::Done => held, + Phase::Asking | Phase::Done | Phase::Upgraded => held, Phase::Head(interims) => self.rx_head(interims, input), Phase::Body(b) => self.rx_body(b, input), } @@ -345,6 +387,15 @@ impl<S: AsRef<[u8]> + AsMut<[u8]>> Client<S> { event: None, }); } + if self.asked == Asked::Upgrade { + // not framed: the upgraded protocol judges it + self.status = u16::try_from(status).ok(); + self.phase = Phase::Upgraded; + return Ok(Rx { + consumed, + event: Some(Event::Response), + }); + } let body = match self.framing(status) { Ok(b) => b, Err(f) => return self.fail(f), @@ -441,7 +492,7 @@ impl<S: AsRef<[u8]> + AsMut<[u8]>> Client<S> { pub const fn rx_closed<'a>(&mut self) -> Result<Option<Event<'a>>, Failure> { match self.phase { Phase::Failed(f) => Err(f), - Phase::Done => Ok(None), + Phase::Done | Phase::Upgraded => Ok(None), Phase::Body(Body::ToClose | Body::EndDue) => { self.phase = Phase::Done; Ok(Some(Event::BodyEnd)) @@ -481,8 +532,10 @@ fn request_head(r: &Request<'_>) -> Result<Own, RespondError> { o.push(origin)?; o.push(b"\r\n")?; } - if !r.pipeline { - o.push(b"connection: close\r\n")?; + match r.connection { + Connection::Close => o.push(b"connection: close\r\n")?, + Connection::KeepAlive => {} + Connection::Upgrade(lines) => o.push(lines)?, } o.push(b"\r\n")?; Ok(o) @@ -532,7 +585,7 @@ mod tests { origin: Some(b"sansio"), scheme: Scheme::Http, no_cache: true, - pipeline: false, + connection: Connection::Close, }, ) .unwrap(); @@ -569,6 +622,40 @@ mod tests { } #[test] + fn an_upgrades_response_is_handed_over_unframed() { + let mut c = Client::new( + [0u8; 1024], + Request { + method: b"GET", + path: b"/x", + host: None, + origin: None, + scheme: Scheme::Http, + no_cache: false, + connection: Connection::Upgrade(b"Upgrade: x\r\nConnection: Upgrade\r\n"), + }, + ) + .unwrap(); + let mut out = [0u8; 512]; + let n = c.tx(&mut out); + assert_eq!( + &out[..n], + b"GET /x HTTP/1.1\r\nUpgrade: x\r\nConnection: Upgrade\r\n\r\n" + ); + // an interim is still dropped; the 101 is final, its frames are not + // a body, and nor would a 200's be + let input = b"HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 101 Go\r\n\r\n\x81\x00"; + let first = c.rx(input).unwrap(); + assert_eq!(first.event, None); + let rx = c.rx(&input[first.consumed..]).unwrap(); + assert_eq!(rx.event, Some(Event::Response)); + assert_eq!(&input[first.consumed + rx.consumed..], b"\x81\x00"); + assert!(c.is_upgraded()); + assert_eq!(c.status(), Some(101)); + assert_eq!(c.rx(b"\x81\x00").unwrap().consumed, 0); + } + + #[test] fn a_body_to_the_close() { let mut c = get(b"GET"); let head = b"HTTP/1.0 200 OK\r\n\r\n"; diff --git a/crates/npro-test/tests/h1_client_replay.rs b/crates/npro-test/tests/h1_client_replay.rs index 7c183c5..e49268a 100644 --- a/crates/npro-test/tests/h1_client_replay.rs +++ b/crates/npro-test/tests/h1_client_replay.rs @@ -17,7 +17,7 @@ // held to clippy's rules for tests #[cfg(test)] mod h1_client_replay { - use npro_h1::client::{Client, Event, Request, Scheme}; + use npro_h1::client::{Client, Connection, Event, Request, Scheme}; use npro_h1::table::DEFAULT_CAPACITY; use npro_test::{StepKind, Transcript, vendored}; @@ -69,7 +69,7 @@ mod h1_client_replay { origin: Some(b"sansio"), scheme: Scheme::Http, no_cache: true, - pipeline: false, + connection: Connection::Close, }, ) .unwrap();
Page fetched 0s ago, creation time: 2ms (vhost etag hits: 0%, cache hits: 0%)