Skip to content

Commit 05925da

Browse files
committed
feat(http1): decouple preserving header case from FFI (fixes hyperium#2313)
The feature is now supported in both the server and the client and can be combined with the title case feature, for headers which don't have entries in the header case map.
1 parent ed2fdb7 commit 05925da

File tree

12 files changed

+634
-167
lines changed

12 files changed

+634
-167
lines changed

src/client/client.rs

+11
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,17 @@ impl Builder {
972972
self
973973
}
974974

975+
/// Set whether HTTP/1 connections will write header names as provided
976+
/// at the socket level.
977+
///
978+
/// Note that this setting does not affect HTTP/2.
979+
///
980+
/// Default is false.
981+
pub fn http1_preserve_header_case(&mut self, val: bool) -> &mut Self {
982+
self.conn_builder.h1_preserve_header_case(val);
983+
self
984+
}
985+
975986
/// Set whether HTTP/0.9 responses should be tolerated.
976987
///
977988
/// Default is false.

src/client/conn.rs

+10
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ pub struct Builder {
124124
pub(super) exec: Exec,
125125
h09_responses: bool,
126126
h1_title_case_headers: bool,
127+
h1_preserve_header_case: bool,
127128
h1_read_buf_exact_size: Option<usize>,
128129
h1_max_buf_size: Option<usize>,
129130
#[cfg(feature = "http2")]
@@ -497,6 +498,7 @@ impl Builder {
497498
h09_responses: false,
498499
h1_read_buf_exact_size: None,
499500
h1_title_case_headers: false,
501+
h1_preserve_header_case: false,
500502
h1_max_buf_size: None,
501503
#[cfg(feature = "http2")]
502504
h2_builder: Default::default(),
@@ -526,6 +528,11 @@ impl Builder {
526528
self
527529
}
528530

531+
pub(crate) fn h1_preserve_header_case(&mut self, enabled: bool) -> &mut Builder {
532+
self.h1_preserve_header_case = enabled;
533+
self
534+
}
535+
529536
pub(super) fn h1_read_buf_exact_size(&mut self, sz: Option<usize>) -> &mut Builder {
530537
self.h1_read_buf_exact_size = sz;
531538
self.h1_max_buf_size = None;
@@ -707,6 +714,9 @@ impl Builder {
707714
if opts.h1_title_case_headers {
708715
conn.set_title_case_headers();
709716
}
717+
if opts.h1_preserve_header_case {
718+
conn.set_preserve_header_case();
719+
}
710720
if opts.h09_responses {
711721
conn.set_h09_responses();
712722
}

src/ext.rs

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//! HTTP extensions
2+
3+
use bytes::Bytes;
4+
#[cfg(feature = "http1")]
5+
use http::header::{GetAll, HeaderName, IntoHeaderName};
6+
use http::HeaderMap;
7+
8+
/// A map from header names to their original casing as received in an HTTP message.
9+
///
10+
/// If an HTTP/1 response `res` is parsed on a connection whose option
11+
/// [`http1_preserve_header_case`] was set to true and the response included
12+
/// the following headers:
13+
///
14+
/// ```ignore
15+
/// x-Bread: Baguette
16+
/// X-BREAD: Pain
17+
/// x-bread: Ficelle
18+
/// ```
19+
///
20+
/// Then `res.extensions().get::<HeaderCaseMap>()` will return a map with:
21+
///
22+
/// ```ignore
23+
/// HeaderCaseMap({
24+
/// "x-bread": ["x-Bread", "X-BREAD", "x-bread"],
25+
/// })
26+
/// ```
27+
///
28+
/// [`http1_preserve_header_case`]: /client/struct.Client.html#method.http1_preserve_header_case
29+
#[derive(Clone, Debug, Default)]
30+
pub struct HeaderCaseMap(HeaderMap<RawHeaderName>);
31+
32+
#[cfg(feature = "http1")]
33+
impl HeaderCaseMap {
34+
/// Returns a view of all spellings associated with that header name,
35+
/// in the order they were found.
36+
pub fn get_all(&self, name: &HeaderName) -> GetAll<'_, RawHeaderName> {
37+
self.0.get_all(name)
38+
}
39+
40+
#[cfg(any(test, feature = "ffi"))]
41+
pub(crate) fn insert(&mut self, name: HeaderName, orig: Bytes) {
42+
self.0.insert(name, RawHeaderName(orig));
43+
}
44+
45+
pub(crate) fn append<N>(&mut self, name: N, orig: Bytes)
46+
where
47+
N: IntoHeaderName,
48+
{
49+
self.0.append(name, RawHeaderName(orig));
50+
}
51+
}
52+
53+
/// Represents a raw header name, which may not in lower case.
54+
///
55+
/// Exclusively used by the `HeaderCaseMap` extension.
56+
#[derive(Clone, Debug)]
57+
pub struct RawHeaderName(Bytes);
58+
59+
#[cfg(feature = "http1")]
60+
impl RawHeaderName {
61+
pub(crate) fn as_bytes(&self) -> &Bytes {
62+
&self.0
63+
}
64+
}

src/ffi/client.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,11 @@ unsafe impl AsTaskType for hyper_clientconn {
106106
ffi_fn! {
107107
/// Creates a new set of HTTP clientconn options to be used in a handshake.
108108
fn hyper_clientconn_options_new() -> *mut hyper_clientconn_options {
109+
let mut builder = conn::Builder::new();
110+
builder.h1_preserve_header_case(true);
111+
109112
Box::into_raw(Box::new(hyper_clientconn_options {
110-
builder: conn::Builder::new(),
113+
builder,
111114
exec: WeakExec::new(),
112115
}))
113116
} ?= std::ptr::null_mut()

src/ffi/http_types.rs

+2-24
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use super::body::hyper_body;
66
use super::error::hyper_code;
77
use super::task::{hyper_task_return_type, AsTaskType};
88
use super::HYPER_ITER_CONTINUE;
9+
use crate::ext::HeaderCaseMap;
910
use crate::header::{HeaderName, HeaderValue};
1011
use crate::{Body, HeaderMap, Method, Request, Response, Uri};
1112

@@ -24,10 +25,6 @@ pub struct hyper_headers {
2425
orig_casing: HeaderCaseMap,
2526
}
2627

27-
// Will probably be moved to `hyper::ext::http1`
28-
#[derive(Debug, Default)]
29-
pub(crate) struct HeaderCaseMap(HeaderMap<Bytes>);
30-
3128
#[derive(Debug)]
3229
pub(crate) struct ReasonPhrase(pub(crate) Bytes);
3330

@@ -294,7 +291,7 @@ ffi_fn! {
294291

295292
for value in headers.headers.get_all(name) {
296293
let (name_ptr, name_len) = if let Some(orig_name) = names.next() {
297-
(orig_name.as_ptr(), orig_name.len())
294+
(orig_name.as_bytes().as_ptr(), orig_name.as_bytes().len())
298295
} else {
299296
(
300297
name.as_str().as_bytes().as_ptr(),
@@ -370,25 +367,6 @@ unsafe fn raw_name_value(
370367
Ok((name, value, orig_name))
371368
}
372369

373-
// ===== impl HeaderCaseMap =====
374-
375-
impl HeaderCaseMap {
376-
pub(crate) fn get_all(&self, name: &HeaderName) -> http::header::GetAll<'_, Bytes> {
377-
self.0.get_all(name)
378-
}
379-
380-
pub(crate) fn insert(&mut self, name: HeaderName, orig: Bytes) {
381-
self.0.insert(name, orig);
382-
}
383-
384-
pub(crate) fn append<N>(&mut self, name: N, orig: Bytes)
385-
where
386-
N: http::header::IntoHeaderName,
387-
{
388-
self.0.append(name, orig);
389-
}
390-
}
391-
392370
#[cfg(test)]
393371
mod tests {
394372
use super::*;

src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ mod cfg;
8080
mod common;
8181
pub mod body;
8282
mod error;
83+
pub mod ext;
8384
#[cfg(test)]
8485
mod mock;
8586
#[cfg(any(feature = "http1", feature = "http2",))]

src/proto/h1/conn.rs

+5-14
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ where
4444
error: None,
4545
keep_alive: KA::Busy,
4646
method: None,
47-
#[cfg(feature = "ffi")]
4847
preserve_header_case: false,
4948
title_case_headers: false,
5049
h09_responses: false,
@@ -74,11 +73,15 @@ where
7473
self.io.set_read_buf_exact_size(sz);
7574
}
7675

77-
#[cfg(feature = "client")]
7876
pub(crate) fn set_title_case_headers(&mut self) {
7977
self.state.title_case_headers = true;
8078
}
8179

80+
#[cfg(feature = "client")]
81+
pub(crate) fn set_preserve_header_case(&mut self) {
82+
self.state.preserve_header_case = true;
83+
}
84+
8285
#[cfg(feature = "client")]
8386
pub(crate) fn set_h09_responses(&mut self) {
8487
self.state.h09_responses = true;
@@ -150,7 +153,6 @@ where
150153
ParseContext {
151154
cached_headers: &mut self.state.cached_headers,
152155
req_method: &mut self.state.method,
153-
#[cfg(feature = "ffi")]
154156
preserve_header_case: self.state.preserve_header_case,
155157
h09_responses: self.state.h09_responses,
156158
}
@@ -488,16 +490,6 @@ where
488490

489491
self.enforce_version(&mut head);
490492

491-
// Maybe check if we should preserve header casing on received
492-
// message headers...
493-
#[cfg(feature = "ffi")]
494-
{
495-
if T::is_client() && !self.state.preserve_header_case {
496-
self.state.preserve_header_case =
497-
head.extensions.get::<crate::ffi::HeaderCaseMap>().is_some();
498-
}
499-
}
500-
501493
let buf = self.io.headers_buf();
502494
match super::role::encode_headers::<T>(
503495
Encode {
@@ -760,7 +752,6 @@ struct State {
760752
/// This is used to know things such as if the message can include
761753
/// a body or not.
762754
method: Option<Method>,
763-
#[cfg(feature = "ffi")]
764755
preserve_header_case: bool,
765756
title_case_headers: bool,
766757
h09_responses: bool,

src/proto/h1/io.rs

-2
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,6 @@ where
159159
ParseContext {
160160
cached_headers: parse_ctx.cached_headers,
161161
req_method: parse_ctx.req_method,
162-
#[cfg(feature = "ffi")]
163162
preserve_header_case: parse_ctx.preserve_header_case,
164163
h09_responses: parse_ctx.h09_responses,
165164
},
@@ -639,7 +638,6 @@ mod tests {
639638
let parse_ctx = ParseContext {
640639
cached_headers: &mut None,
641640
req_method: &mut None,
642-
#[cfg(feature = "ffi")]
643641
preserve_header_case: false,
644642
h09_responses: false,
645643
};

src/proto/h1/mod.rs

-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ pub(crate) struct ParsedMessage<T> {
7070
pub(crate) struct ParseContext<'a> {
7171
cached_headers: &'a mut Option<HeaderMap>,
7272
req_method: &'a mut Option<Method>,
73-
#[cfg(feature = "ffi")]
7473
preserve_header_case: bool,
7574
h09_responses: bool,
7675
}

0 commit comments

Comments
 (0)