From a7a3438c5d7267ff952dfc909ce5ee43a8f63310 Mon Sep 17 00:00:00 2001 From: CinisBorn Date: Mon, 10 Aug 2026 17:28:02 -0300 Subject: [PATCH 1/2] docs(clippy): fix missing_errors_doc warnings --- Cargo.toml | 1 - src/client/conn/http1.rs | 21 ++++++++++++++++++++- src/client/conn/http2.rs | 17 ++++++++++++++++- src/upgrade.rs | 6 ++++-- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 22826d9900..150b030a75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,7 +131,6 @@ map_err_ignore = "allow" map_unwrap_or = "allow" match_wild_err_arm = "allow" missing_fields_in_debug = "allow" # TODO: use finish_non_exhaustive -missing_errors_doc = "allow" # TODO: good to fix missing_panics_doc = "allow" # TODO: might be false multiple_inherent_impl = "allow" multiple_unsafe_ops_per_block = "allow" diff --git a/src/client/conn/http1.rs b/src/client/conn/http1.rs index ec64543c23..7468507623 100644 --- a/src/client/conn/http1.rs +++ b/src/client/conn/http1.rs @@ -97,6 +97,10 @@ where /// Prevent shutdown of the underlying IO object at the end of service the request, /// instead run `into_parts`. This is a convenience wrapper over `poll_without_shutdown`. + /// + /// # Errors + /// + /// Returns an error if the connection encounters an error while being polled to completion. pub async fn without_shutdown(self) -> crate::Result> { let mut conn = Some(self); crate::common::future::poll_fn(move |cx| -> Poll>> { @@ -137,6 +141,10 @@ pub struct Builder { /// /// This is a shortcut for `Builder::new().handshake(io)`. /// See [`client::conn`](crate::client::conn) for more. +/// +/// # Errors +/// +/// Returns an error if the HTTP/1 connection handshake fails. pub async fn handshake(io: T) -> crate::Result<(SendRequest, Connection)> where T: Read + Write + Unpin, @@ -159,6 +167,8 @@ impl SendRequest { /// Waits until the dispatcher is ready. /// + /// # Errors + /// /// If the associated connection is closed, this returns an Error. pub async fn ready(&mut self) -> crate::Result<()> { crate::common::future::poll_fn(|cx| self.poll_ready(cx)).await @@ -210,6 +220,11 @@ where /// hyper closes the underlying connection when a request future is /// dropped before completion. Any subsequent calls on the same /// [`SendRequest`] will return a `canceled` error. + /// + /// # Errors + /// + /// Returns an error if the connection is not ready or if an error occurs while + /// processing the request. pub fn send_request( &mut self, req: Request, @@ -236,7 +251,7 @@ where /// /// Returns a future that if successful, yields the `Response`. /// - /// # Error + /// # Errors /// /// If there was an error before trying to serialize the request to the /// connection, the message will be returned as part of this error. @@ -533,6 +548,10 @@ impl Builder { /// /// Note, if [`Connection`] is not `await`-ed, [`SendRequest`] will /// do nothing. + /// + /// # Errors + /// + /// Returns an error if the HTTP/1connection handshake fails. pub fn handshake( &self, io: T, diff --git a/src/client/conn/http2.rs b/src/client/conn/http2.rs index ec046e0a39..d1dfcfd416 100644 --- a/src/client/conn/http2.rs +++ b/src/client/conn/http2.rs @@ -74,6 +74,10 @@ pub struct Builder { /// /// This is a shortcut for `Builder::new(exec).handshake(io)`. /// See [`client::conn`](crate::client::conn) for more. +/// +/// # Errors +/// +/// Returns an error if the HTTP/2 connection handshake fails. pub async fn handshake( exec: E, io: T, @@ -104,6 +108,8 @@ impl SendRequest { /// Waits until the dispatcher is ready. /// + /// # Errors + /// /// If the associated connection is closed, this returns an Error. pub async fn ready(&mut self) -> crate::Result<()> { crate::common::future::poll_fn(|cx| self.poll_ready(cx)).await @@ -147,6 +153,11 @@ where /// other in-flight and future requests. The peer is notified /// immediately rather than continuing to send a response body that /// would be discarded. + /// + /// # Errors + /// + /// Returns an error if the connection is not ready or if an error occurs while + /// processing the request. pub fn send_request( &mut self, req: Request, @@ -174,7 +185,7 @@ where /// /// Returns a future that if successful, yields the `Response`. /// - /// # Error + /// # Errors /// /// If there was an error before trying to serialize the request to the /// connection, the message will be returned as part of this error. @@ -547,6 +558,10 @@ where /// /// Note, if [`Connection`] is not `await`-ed, [`SendRequest`] will /// do nothing. + /// + /// # Errors + /// + /// Returns an error if the HTTP/2 connection handshake fails. pub fn handshake( &self, io: T, diff --git a/src/upgrade.rs b/src/upgrade.rs index df1b9e6847..bb350e78ed 100644 --- a/src/upgrade.rs +++ b/src/upgrade.rs @@ -147,8 +147,10 @@ impl Upgraded { /// Tries to downcast the internal trait object to the type passed. /// - /// On success, returns the downcasted parts. On error, returns the - /// `Upgraded` back. + /// On success, returns the downcasted parts. + /// + /// # Errors + /// On error, returns the `Upgraded` back. pub fn downcast(self) -> Result, Self> { let (io, buf) = self.io.into_inner(); match io.__hyper_downcast() { From 9940bd01d8995f9fd18b2b1756de07c0e5c59dab Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Tue, 11 Aug 2026 03:16:47 -0400 Subject: [PATCH 2/2] fix(http1): detect TE: trailers caselessly and with other values (#4152) --- src/headers.rs | 52 +++++++++++++++++++++++++++++++ src/proto/h1/conn.rs | 8 ++--- tests/server.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 6 deletions(-) diff --git a/src/headers.rs b/src/headers.rs index 89b70fd45e..caace71f2a 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -42,6 +42,26 @@ fn connection_has(value: &HeaderValue, needle: &str) -> bool { false } +#[cfg(feature = "http1")] +pub(super) fn te_is_trailers(headers: &http::HeaderMap) -> bool { + header_value_list_has(headers.get_all(http::header::TE).into_iter(), "trailers") +} + +#[cfg(feature = "http1")] +fn header_value_list_has(values: http::header::ValueIter<'_, HeaderValue>, needle: &str) -> bool { + for value in values { + if let Ok(line) = value.to_str() { + for token in line.split(',') { + if token.trim().eq_ignore_ascii_case(needle) { + return true; + } + } + } + } + + false +} + #[cfg(all(feature = "http1", feature = "server"))] pub(super) fn content_length_parse(value: &HeaderValue) -> Option { from_digits(value.as_bytes()) @@ -166,3 +186,35 @@ pub(super) fn add_chunked(mut entry: http::header::OccupiedEntry<'_, HeaderValue entry.insert(HeaderValue::from_static(CHUNKED)); } + +#[cfg(all(test, feature = "http1"))] +mod tests { + use super::te_is_trailers; + use http::header::{HeaderValue, TE}; + use http::HeaderMap; + + #[test] + fn te_is_trailers_accepts_comma_separated_values() { + let mut headers = HeaderMap::new(); + headers.insert(TE, HeaderValue::from_static("gzip, Trailers")); + + assert!(te_is_trailers(&headers)); + } + + #[test] + fn te_is_trailers_accepts_multiple_header_lines() { + let mut headers = HeaderMap::new(); + headers.append(TE, HeaderValue::from_static("gzip")); + headers.append(TE, HeaderValue::from_static("trailers")); + + assert!(te_is_trailers(&headers)); + } + + #[test] + fn te_is_trailers_rejects_missing_trailers_token() { + let mut headers = HeaderMap::new(); + headers.insert(TE, HeaderValue::from_static("gzip")); + + assert!(!te_is_trailers(&headers)); + } +} diff --git a/src/proto/h1/conn.rs b/src/proto/h1/conn.rs index a2d2670e6b..19c1283df7 100644 --- a/src/proto/h1/conn.rs +++ b/src/proto/h1/conn.rs @@ -11,7 +11,7 @@ use std::time::Duration; use crate::rt::{Read, Write}; use bytes::{Buf, Bytes}; use futures_core::ready; -use http::header::{HeaderValue, CONNECTION, TE}; +use http::header::{HeaderValue, CONNECTION}; use http::{HeaderMap, Method, Version}; use http_body::Frame; use httparse::ParserConfig; @@ -325,11 +325,7 @@ where )); } - self.state.allow_trailer_fields = msg - .head - .headers - .get(TE) - .map_or(false, |te_header| te_header == "trailers"); + self.state.allow_trailer_fields = headers::te_is_trailers(&msg.head.headers); Poll::Ready(Some(Ok((msg.head, msg.decode, wants)))) } diff --git a/tests/server.rs b/tests/server.rs index 855840b388..1843610e88 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -3335,6 +3335,79 @@ fn http1_trailer_fields_not_allowed() { assert_eq!(body, expected_body); } +#[test] +fn http1_trailer_fields_allowed_with_comma_separated_te() { + let body = futures_util::stream::once(async move { Ok("hello".into()) }); + let mut headers = HeaderMap::new(); + headers.insert("chunky-trailer", "header data".parse().unwrap()); + + let server = serve(); + server + .reply() + .header("transfer-encoding", "chunked") + .header("trailer", "chunky-trailer") + .body_stream_with_trailers(body, headers); + let mut req = connect(server.addr()); + req.write_all( + b"\ + GET / HTTP/1.1\r\n\ + Host: example.domain\r\n\ + Connection: keep-alive\r\n\ + TE: gzip, Trailers\r\n\ + \r\n\ + ", + ) + .expect("writing"); + + let chunky_trailer_chunk = b"\r\nchunky-trailer: header data\r\n\r\n"; + let res = read_until(&mut req, |buf| buf.ends_with(chunky_trailer_chunk)).expect("reading"); + let sres = s(&res); + + let date_fragment = "GMT\r\n\r\n"; + let pos = sres.find(date_fragment).expect("find GMT"); + let body = &sres[pos + date_fragment.len()..]; + + let expected_body = "5\r\nhello\r\n0\r\nchunky-trailer: header data\r\n\r\n"; + assert_eq!(body, expected_body); +} + +#[test] +fn http1_trailer_fields_allowed_with_multiple_te_headers() { + let body = futures_util::stream::once(async move { Ok("hello".into()) }); + let mut headers = HeaderMap::new(); + headers.insert("chunky-trailer", "header data".parse().unwrap()); + + let server = serve(); + server + .reply() + .header("transfer-encoding", "chunked") + .header("trailer", "chunky-trailer") + .body_stream_with_trailers(body, headers); + let mut req = connect(server.addr()); + req.write_all( + b"\ + GET / HTTP/1.1\r\n\ + Host: example.domain\r\n\ + Connection: keep-alive\r\n\ + TE: gzip\r\n\ + TE: trailers\r\n\ + \r\n\ + ", + ) + .expect("writing"); + + let chunky_trailer_chunk = b"\r\nchunky-trailer: header data\r\n\r\n"; + let res = read_until(&mut req, |buf| buf.ends_with(chunky_trailer_chunk)).expect("reading"); + let sres = s(&res); + + let date_fragment = "GMT\r\n\r\n"; + let pos = sres.find(date_fragment).expect("find GMT"); + let body = &sres[pos + date_fragment.len()..]; + + let expected_body = "5\r\nhello\r\n0\r\nchunky-trailer: header data\r\n\r\n"; + assert_eq!(body, expected_body); +} + #[test] fn http1_trailer_recv_fields() { let server = serve();