| Author | Andy Green <andy@warmcat.com> 2026-10-05 16:38 UTC | | Committer | Andy Green <andy@warmcat.com> 2026-10-05 19:18 UTC | | Tree | bb0fc83488b1e189879dba1cf9bfeedb3635c91c 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();
|