Skip to content

Commit 075bfa0

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 075bfa0

File tree

12 files changed

+654
-177
lines changed

12 files changed

+654
-177
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::{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 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 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_bytes().as_ptr(), orig_name.as_bytes().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+
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)