Skip to content

Commit bd48b82

Browse files
committed
fix: canonicalize Host header into :authority for outbound HTTP/2
When sending HTTP/2 requests or push promises, promote the first valid Host header value to :authority and strip all Host headers from regular fields. This prevents emitting conflicting :authority and Host on the wire, aligning with RFC 9113 and the behavior of other HTTP/2 client implementations such as curl, Go's std, and Python's httpx. Fixes #876
1 parent 5634ddd commit bd48b82

File tree

7 files changed

+481
-8
lines changed

7 files changed

+481
-8
lines changed

src/client.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@
137137
138138
use crate::codec::{Codec, SendError, UserError};
139139
use crate::ext::Protocol;
140-
use crate::frame::{Headers, Pseudo, Reason, Settings, StreamId};
140+
use crate::frame::{self, Headers, Pseudo, Reason, Settings, StreamId};
141141
use crate::proto::{self, Error};
142142
use crate::{FlowControl, PingPong, RecvStream, SendStream};
143143

@@ -428,10 +428,11 @@ where
428428
/// value of its version field. If the version is set to 2.0, then the
429429
/// request is encoded as per the specification recommends.
430430
///
431-
/// If the version is set to a lower value, then the request is encoded to
432-
/// preserve the characteristics of HTTP 1.1 and lower. Specifically, host
433-
/// headers are permitted and the `:authority` pseudo header is not
434-
/// included.
431+
/// If the version is set to a lower value, then request-target handling is
432+
/// made compatible with HTTP/1.x-style inputs (for example, relative URIs
433+
/// are accepted). Headers are still normalized for HTTP/2 transmission:
434+
/// `Host` is canonicalized into `:authority` and removed from regular
435+
/// headers.
435436
///
436437
/// The caller should always set the request's version field to 2.0 unless
437438
/// specifically transmitting an HTTP 1.1 request over 2.0.
@@ -1613,7 +1614,7 @@ impl Peer {
16131614
Parts {
16141615
method,
16151616
uri,
1616-
headers,
1617+
mut headers,
16171618
version,
16181619
..
16191620
},
@@ -1654,6 +1655,9 @@ impl Peer {
16541655
}
16551656
}
16561657

1658+
// Canonicalize Host header into :authority for HTTP/2
1659+
frame::canonicalize_host_authority(&mut pseudo, &mut headers);
1660+
16571661
// Create the HEADERS frame
16581662
let mut frame = Headers::new(id, pseudo, headers);
16591663

src/frame/headers.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@ use crate::hpack::{self, BytesStr};
66
use http::header::{self, HeaderName, HeaderValue};
77
use http::{uri, HeaderMap, Method, Request, StatusCode, Uri};
88

9+
/// Canonicalize `Host` header into `:authority` pseudo-header for HTTP/2.
10+
///
11+
/// - If a `Host` header is present, attempt to parse its first value as a URI authority.
12+
/// - On success, override `:authority` with the parsed value.
13+
/// - Always remove all `Host` headers from the regular header map.
14+
///
15+
/// Callers should only invoke this in an HTTP/2 context.
16+
pub(crate) fn canonicalize_host_authority(pseudo: &mut Pseudo, headers: &mut HeaderMap) {
17+
if let Some(host) = headers.get(header::HOST) {
18+
if let Ok(authority) = uri::Authority::from_maybe_shared(host.as_bytes().to_vec()) {
19+
pseudo.set_authority(BytesStr::from(authority.as_str()));
20+
}
21+
}
22+
23+
headers.remove(header::HOST);
24+
}
25+
926
use bytes::{Buf, BufMut, Bytes, BytesMut};
1027

1128
use std::fmt;

src/frame/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ mod window_update;
5151
pub use self::data::Data;
5252
pub use self::go_away::GoAway;
5353
pub use self::head::{Head, Kind};
54+
pub(crate) use self::headers::canonicalize_host_authority;
5455
pub use self::headers::{
5556
parse_u64, Continuation, Headers, Pseudo, PushPromise, PushPromiseHeaderError,
5657
};

src/server.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1581,13 +1581,16 @@ impl Peer {
15811581
Parts {
15821582
method,
15831583
uri,
1584-
headers,
1584+
mut headers,
15851585
..
15861586
},
15871587
_,
15881588
) = request.into_parts();
15891589

1590-
let pseudo = Pseudo::request(method, uri, None);
1590+
let mut pseudo = Pseudo::request(method, uri, None);
1591+
1592+
// Canonicalize Host header into :authority for HTTP/2 push promises
1593+
frame::canonicalize_host_authority(&mut pseudo, &mut headers);
15911594

15921595
Ok(frame::PushPromise::new(
15931596
stream_id,

tests/h2-support/src/frames.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,12 @@ impl Mock<frame::PushPromise> {
243243
Mock(frame)
244244
}
245245

246+
pub fn pseudo(self, pseudo: frame::Pseudo) -> Self {
247+
let (id, promised, _, fields) = self.into_parts();
248+
let frame = frame::PushPromise::new(id, promised, pseudo, fields);
249+
Mock(frame)
250+
}
251+
246252
pub fn fields(self, fields: HeaderMap) -> Self {
247253
let (id, promised, pseudo, _) = self.into_parts();
248254
let frame = frame::PushPromise::new(id, promised, pseudo, fields);

tests/h2-tests/tests/client_request.rs

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2059,6 +2059,255 @@ async fn reset_before_headers_reaches_peer_without_headers() {
20592059
join(srv, client).await;
20602060
}
20612061

2062+
#[tokio::test]
2063+
async fn host_authority_mismatch_promotes_host_to_authority() {
2064+
// When Host differs from URI authority, Host should win for :authority
2065+
// and Host header should be stripped from regular headers.
2066+
h2_support::trace_init!();
2067+
let (io, mut srv) = mock::new();
2068+
2069+
let srv = async move {
2070+
let settings = srv.assert_client_handshake().await;
2071+
assert_default_settings!(settings);
2072+
srv.recv_frame(
2073+
frames::headers(1)
2074+
.pseudo(frame::Pseudo {
2075+
method: Method::GET.into(),
2076+
scheme: util::byte_str("https").into(),
2077+
authority: util::byte_str("example.com").into(),
2078+
path: util::byte_str("/").into(),
2079+
..Default::default()
2080+
})
2081+
.eos(),
2082+
)
2083+
.await;
2084+
srv.send_frame(frames::headers(1).response(200).eos()).await;
2085+
};
2086+
2087+
let h2 = async move {
2088+
let (mut client, mut h2) = client::handshake(io).await.expect("handshake");
2089+
2090+
let request = Request::builder()
2091+
.version(Version::HTTP_2)
2092+
.method(Method::GET)
2093+
.uri("https://example.net/")
2094+
.header("host", "example.com")
2095+
.body(())
2096+
.unwrap();
2097+
2098+
let (response, _) = client.send_request(request, true).unwrap();
2099+
h2.drive(response).await.unwrap();
2100+
};
2101+
2102+
join(srv, h2).await;
2103+
}
2104+
2105+
#[tokio::test]
2106+
async fn host_authority_http11_version_still_promotes_host_to_authority() {
2107+
// Real integration path: higher layers (for example hyper/hyper-util)
2108+
// commonly pass Request values with the default HTTP/1.1 version even when
2109+
// the selected transport is HTTP/2. We still need HTTP/2-compliant
2110+
// canonicalization on the wire, so Host must promote to :authority here.
2111+
h2_support::trace_init!();
2112+
let (io, mut srv) = mock::new();
2113+
2114+
let srv = async move {
2115+
let settings = srv.assert_client_handshake().await;
2116+
assert_default_settings!(settings);
2117+
srv.recv_frame(
2118+
frames::headers(1)
2119+
.pseudo(frame::Pseudo {
2120+
method: Method::GET.into(),
2121+
scheme: util::byte_str("https").into(),
2122+
authority: util::byte_str("example.com").into(),
2123+
path: util::byte_str("/").into(),
2124+
..Default::default()
2125+
})
2126+
.eos(),
2127+
)
2128+
.await;
2129+
srv.send_frame(frames::headers(1).response(200).eos()).await;
2130+
};
2131+
2132+
let h2 = async move {
2133+
let (mut client, mut h2) = client::handshake(io).await.expect("handshake");
2134+
2135+
let request = Request::builder()
2136+
// Keep the default request version to model the common caller behavior.
2137+
.method(Method::GET)
2138+
.uri("https://example.net/")
2139+
.header("host", "example.com")
2140+
.body(())
2141+
.unwrap();
2142+
2143+
let (response, _) = client.send_request(request, true).unwrap();
2144+
h2.drive(response).await.unwrap();
2145+
};
2146+
2147+
join(srv, h2).await;
2148+
}
2149+
2150+
#[tokio::test]
2151+
async fn host_authority_matching_strips_host() {
2152+
// When Host matches URI authority, Host header should still be stripped.
2153+
h2_support::trace_init!();
2154+
let (io, mut srv) = mock::new();
2155+
2156+
let srv = async move {
2157+
let settings = srv.assert_client_handshake().await;
2158+
assert_default_settings!(settings);
2159+
srv.recv_frame(
2160+
frames::headers(1)
2161+
.pseudo(frame::Pseudo {
2162+
method: Method::GET.into(),
2163+
scheme: util::byte_str("https").into(),
2164+
authority: util::byte_str("example.com").into(),
2165+
path: util::byte_str("/").into(),
2166+
..Default::default()
2167+
})
2168+
.eos(),
2169+
)
2170+
.await;
2171+
srv.send_frame(frames::headers(1).response(200).eos()).await;
2172+
};
2173+
2174+
let h2 = async move {
2175+
let (mut client, mut h2) = client::handshake(io).await.expect("handshake");
2176+
2177+
let request = Request::builder()
2178+
.version(Version::HTTP_2)
2179+
.method(Method::GET)
2180+
.uri("https://example.com/")
2181+
.header("host", "example.com")
2182+
.body(())
2183+
.unwrap();
2184+
2185+
let (response, _) = client.send_request(request, true).unwrap();
2186+
h2.drive(response).await.unwrap();
2187+
};
2188+
2189+
join(srv, h2).await;
2190+
}
2191+
2192+
#[tokio::test]
2193+
async fn host_authority_duplicate_host_first_wins() {
2194+
// When multiple Host headers are present, first value is used for :authority.
2195+
h2_support::trace_init!();
2196+
let (io, mut srv) = mock::new();
2197+
2198+
let srv = async move {
2199+
let settings = srv.assert_client_handshake().await;
2200+
assert_default_settings!(settings);
2201+
srv.recv_frame(
2202+
frames::headers(1)
2203+
.pseudo(frame::Pseudo {
2204+
method: Method::GET.into(),
2205+
scheme: util::byte_str("https").into(),
2206+
authority: util::byte_str("first.example").into(),
2207+
path: util::byte_str("/").into(),
2208+
..Default::default()
2209+
})
2210+
.eos(),
2211+
)
2212+
.await;
2213+
srv.send_frame(frames::headers(1).response(200).eos()).await;
2214+
};
2215+
2216+
let h2 = async move {
2217+
let (mut client, mut h2) = client::handshake(io).await.expect("handshake");
2218+
2219+
let request = Request::builder()
2220+
.version(Version::HTTP_2)
2221+
.method(Method::GET)
2222+
.uri("https://example.net/")
2223+
.header("host", "first.example")
2224+
.header("host", "second.example")
2225+
.body(())
2226+
.unwrap();
2227+
2228+
let (response, _) = client.send_request(request, true).unwrap();
2229+
h2.drive(response).await.unwrap();
2230+
};
2231+
2232+
join(srv, h2).await;
2233+
}
2234+
2235+
#[tokio::test]
2236+
async fn host_authority_invalid_host_keeps_uri_authority() {
2237+
// When Host is invalid (unparseable as authority), keep URI authority and strip Host.
2238+
h2_support::trace_init!();
2239+
let (io, mut srv) = mock::new();
2240+
2241+
let srv = async move {
2242+
let settings = srv.assert_client_handshake().await;
2243+
assert_default_settings!(settings);
2244+
srv.recv_frame(
2245+
frames::headers(1)
2246+
.pseudo(frame::Pseudo {
2247+
method: Method::GET.into(),
2248+
scheme: util::byte_str("https").into(),
2249+
authority: util::byte_str("example.net").into(),
2250+
path: util::byte_str("/").into(),
2251+
..Default::default()
2252+
})
2253+
.eos(),
2254+
)
2255+
.await;
2256+
srv.send_frame(frames::headers(1).response(200).eos()).await;
2257+
};
2258+
2259+
let h2 = async move {
2260+
let (mut client, mut h2) = client::handshake(io).await.expect("handshake");
2261+
2262+
let request = Request::builder()
2263+
.version(Version::HTTP_2)
2264+
.method(Method::GET)
2265+
.uri("https://example.net/")
2266+
.header("host", "not:a/good authority")
2267+
.body(())
2268+
.unwrap();
2269+
2270+
let (response, _) = client.send_request(request, true).unwrap();
2271+
h2.drive(response).await.unwrap();
2272+
};
2273+
2274+
join(srv, h2).await;
2275+
}
2276+
2277+
#[tokio::test]
2278+
async fn host_authority_relative_uri_http2_still_errors() {
2279+
// Relative URI with HTTP/2 version should still produce MissingUriSchemeAndAuthority error,
2280+
// even when a Host header is provided. The Host canonicalization does not synthesize
2281+
// authority for relative URIs.
2282+
h2_support::trace_init!();
2283+
let (io, mut srv) = mock::new();
2284+
2285+
let srv = async move {
2286+
let settings = srv.assert_client_handshake().await;
2287+
assert_default_settings!(settings);
2288+
};
2289+
2290+
let h2 = async move {
2291+
let (mut client, h2) = client::handshake(io).await.expect("handshake");
2292+
2293+
let request = Request::builder()
2294+
.version(Version::HTTP_2)
2295+
.method(Method::GET)
2296+
.uri("/")
2297+
.header("host", "example.com")
2298+
.body(())
2299+
.unwrap();
2300+
2301+
client
2302+
.send_request(request, true)
2303+
.expect_err("should be UserError");
2304+
let _: () = h2.await.expect("h2");
2305+
drop(client);
2306+
};
2307+
2308+
join(srv, h2).await;
2309+
}
2310+
20622311
const SETTINGS: &[u8] = &[0, 0, 0, 4, 0, 0, 0, 0, 0];
20632312
const SETTINGS_ACK: &[u8] = &[0, 0, 0, 4, 1, 0, 0, 0, 0];
20642313

0 commit comments

Comments
 (0)