Skip to content

Commit d853f52

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 117cc49 commit d853f52

File tree

12 files changed

+656
-181
lines changed

12 files changed

+656
-181
lines changed

src/client/client.rs

+11
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,17 @@ impl Builder {
997997
self
998998
}
999999

1000+
/// Set whether HTTP/1 connections will write header names as provided
1001+
/// at the socket level.
1002+
///
1003+
/// Note that this setting does not affect HTTP/2.
1004+
///
1005+
/// Default is false.
1006+
pub fn http1_preserve_header_case(&mut self, val: bool) -> &mut Self {
1007+
self.conn_builder.h1_preserve_header_case(val);
1008+
self
1009+
}
1010+
10001011
/// Set whether HTTP/0.9 responses should be tolerated.
10011012
///
10021013
/// Default is false.

src/client/conn.rs

+10
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ pub struct Builder {
126126
h09_responses: bool,
127127
h1_parser_config: ParserConfig,
128128
h1_title_case_headers: bool,
129+
h1_preserve_header_case: bool,
129130
h1_read_buf_exact_size: Option<usize>,
130131
h1_max_buf_size: Option<usize>,
131132
#[cfg(feature = "http2")]
@@ -500,6 +501,7 @@ impl Builder {
500501
h1_read_buf_exact_size: None,
501502
h1_parser_config: Default::default(),
502503
h1_title_case_headers: false,
504+
h1_preserve_header_case: false,
503505
h1_max_buf_size: None,
504506
#[cfg(feature = "http2")]
505507
h2_builder: Default::default(),
@@ -537,6 +539,11 @@ impl Builder {
537539
self
538540
}
539541

542+
pub(crate) fn h1_preserve_header_case(&mut self, enabled: bool) -> &mut Builder {
543+
self.h1_preserve_header_case = enabled;
544+
self
545+
}
546+
540547
pub(super) fn h1_read_buf_exact_size(&mut self, sz: Option<usize>) -> &mut Builder {
541548
self.h1_read_buf_exact_size = sz;
542549
self.h1_max_buf_size = None;
@@ -719,6 +726,9 @@ impl Builder {
719726
if opts.h1_title_case_headers {
720727
conn.set_title_case_headers();
721728
}
729+
if opts.h1_preserve_header_case {
730+
conn.set_preserve_header_case();
731+
}
722732
if opts.h09_responses {
723733
conn.set_h09_responses();
724734
}

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::{HeaderName, IntoHeaderName, ValueIter};
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)]
30+
pub(crate) struct HeaderCaseMap(HeaderMap<Bytes>);
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(crate) fn get_all<'a>(
37+
&'a self,
38+
name: &HeaderName,
39+
) -> impl Iterator<Item = impl AsRef<[u8]> + 'a> + 'a {
40+
self.get_all_internal(name).into_iter()
41+
}
42+
43+
/// Returns a view of all spellings associated with that header name,
44+
/// in the order they were found.
45+
pub(crate) fn get_all_internal<'a>(&'a self, name: &HeaderName) -> ValueIter<'_, Bytes> {
46+
self.0.get_all(name).into_iter()
47+
}
48+
49+
pub(crate) fn default() -> Self {
50+
Self(Default::default())
51+
}
52+
53+
#[cfg(any(test, feature = "ffi"))]
54+
pub(crate) fn insert(&mut self, name: HeaderName, orig: Bytes) {
55+
self.0.insert(name, orig);
56+
}
57+
58+
pub(crate) fn append<N>(&mut self, name: N, orig: Bytes)
59+
where
60+
N: IntoHeaderName,
61+
{
62+
self.0.append(name, orig);
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

+14-31
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

@@ -18,16 +19,11 @@ pub struct hyper_response(pub(super) Response<Body>);
1819
/// An HTTP header map.
1920
///
2021
/// These can be part of a request or response.
21-
#[derive(Default)]
2222
pub struct hyper_headers {
2323
pub(super) headers: HeaderMap,
2424
orig_casing: HeaderCaseMap,
2525
}
2626

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

@@ -229,7 +225,7 @@ impl hyper_response {
229225
let orig_casing = resp
230226
.extensions_mut()
231227
.remove::<HeaderCaseMap>()
232-
.unwrap_or_default();
228+
.unwrap_or_else(HeaderCaseMap::default);
233229
resp.extensions_mut().insert(hyper_headers {
234230
headers,
235231
orig_casing,
@@ -265,10 +261,7 @@ type hyper_headers_foreach_callback =
265261
impl hyper_headers {
266262
pub(super) fn get_or_default(ext: &mut http::Extensions) -> &mut hyper_headers {
267263
if let None = ext.get_mut::<hyper_headers>() {
268-
ext.insert(hyper_headers {
269-
headers: Default::default(),
270-
orig_casing: Default::default(),
271-
});
264+
ext.insert(hyper_headers::default());
272265
}
273266

274267
ext.get_mut::<hyper_headers>().unwrap()
@@ -290,11 +283,11 @@ ffi_fn! {
290283
//
291284
// TODO: consider adding http::HeaderMap::entries() iterator
292285
for name in headers.headers.keys() {
293-
let mut names = headers.orig_casing.get_all(name).iter();
286+
let mut names = headers.orig_casing.get_all(name);
294287

295288
for value in headers.headers.get_all(name) {
296289
let (name_ptr, name_len) = if let Some(orig_name) = names.next() {
297-
(orig_name.as_ptr(), orig_name.len())
290+
(orig_name.as_ref().as_ptr(), orig_name.as_ref().len())
298291
} else {
299292
(
300293
name.as_str().as_bytes().as_ptr(),
@@ -349,6 +342,15 @@ ffi_fn! {
349342
}
350343
}
351344

345+
impl Default for hyper_headers {
346+
fn default() -> Self {
347+
Self {
348+
headers: Default::default(),
349+
orig_casing: HeaderCaseMap::default(),
350+
}
351+
}
352+
}
353+
352354
unsafe fn raw_name_value(
353355
name: *const u8,
354356
name_len: size_t,
@@ -370,25 +372,6 @@ unsafe fn raw_name_value(
370372
Ok((name, value, orig_name))
371373
}
372374

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-
392375
#[cfg(test)]
393376
mod tests {
394377
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+
mod ext;
8384
#[cfg(test)]
8485
mod mock;
8586
#[cfg(any(feature = "http1", feature = "http2",))]

src/proto/h1/conn.rs

+6-16
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ where
4646
keep_alive: KA::Busy,
4747
method: None,
4848
h1_parser_config: ParserConfig::default(),
49-
#[cfg(feature = "ffi")]
5049
preserve_header_case: false,
5150
title_case_headers: false,
5251
h09_responses: false,
@@ -77,13 +76,16 @@ where
7776
}
7877

7978
#[cfg(feature = "client")]
79+
pub(crate) fn set_h1_parser_config(&mut self, parser_config: ParserConfig) {
80+
self.state.h1_parser_config = parser_config;
81+
}
82+
8083
pub(crate) fn set_title_case_headers(&mut self) {
8184
self.state.title_case_headers = true;
8285
}
8386

84-
#[cfg(feature = "client")]
85-
pub(crate) fn set_h1_parser_config(&mut self, parser_config: ParserConfig) {
86-
self.state.h1_parser_config = parser_config;
87+
pub(crate) fn set_preserve_header_case(&mut self) {
88+
self.state.preserve_header_case = true;
8789
}
8890

8991
#[cfg(feature = "client")]
@@ -158,7 +160,6 @@ where
158160
cached_headers: &mut self.state.cached_headers,
159161
req_method: &mut self.state.method,
160162
h1_parser_config: self.state.h1_parser_config.clone(),
161-
#[cfg(feature = "ffi")]
162163
preserve_header_case: self.state.preserve_header_case,
163164
h09_responses: self.state.h09_responses,
164165
}
@@ -499,16 +500,6 @@ where
499500

500501
self.enforce_version(&mut head);
501502

502-
// Maybe check if we should preserve header casing on received
503-
// message headers...
504-
#[cfg(feature = "ffi")]
505-
{
506-
if T::is_client() && !self.state.preserve_header_case {
507-
self.state.preserve_header_case =
508-
head.extensions.get::<crate::ffi::HeaderCaseMap>().is_some();
509-
}
510-
}
511-
512503
let buf = self.io.headers_buf();
513504
match super::role::encode_headers::<T>(
514505
Encode {
@@ -772,7 +763,6 @@ struct State {
772763
/// a body or not.
773764
method: Option<Method>,
774765
h1_parser_config: ParserConfig,
775-
#[cfg(feature = "ffi")]
776766
preserve_header_case: bool,
777767
title_case_headers: bool,
778768
h09_responses: bool,

src/proto/h1/io.rs

-2
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,6 @@ where
160160
cached_headers: parse_ctx.cached_headers,
161161
req_method: parse_ctx.req_method,
162162
h1_parser_config: parse_ctx.h1_parser_config.clone(),
163-
#[cfg(feature = "ffi")]
164163
preserve_header_case: parse_ctx.preserve_header_case,
165164
h09_responses: parse_ctx.h09_responses,
166165
},
@@ -644,7 +643,6 @@ mod tests {
644643
cached_headers: &mut None,
645644
req_method: &mut None,
646645
h1_parser_config: Default::default(),
647-
#[cfg(feature = "ffi")]
648646
preserve_header_case: false,
649647
h09_responses: false,
650648
};

src/proto/h1/mod.rs

-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ pub(crate) struct ParseContext<'a> {
7272
cached_headers: &'a mut Option<HeaderMap>,
7373
req_method: &'a mut Option<Method>,
7474
h1_parser_config: ParserConfig,
75-
#[cfg(feature = "ffi")]
7675
preserve_header_case: bool,
7776
h09_responses: bool,
7877
}

0 commit comments

Comments
 (0)