Skip to content

Commit a02fec8

Browse files
mbilkerseanmonstar
authored andcommittedApr 24, 2018
feat(client): add support for title case header names (#1497)
This introduces support for the HTTP/1 Client to write header names as title case when encoding the request. Closes #1492
1 parent 2cd4666 commit a02fec8

File tree

6 files changed

+174
-4
lines changed

6 files changed

+174
-4
lines changed
 

‎src/client/conn.rs

+10
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ where
6767
pub struct Builder {
6868
exec: Exec,
6969
h1_writev: bool,
70+
h1_title_case_headers: bool,
7071
http2: bool,
7172
}
7273

@@ -419,6 +420,7 @@ impl Builder {
419420
Builder {
420421
exec: Exec::Default,
421422
h1_writev: true,
423+
h1_title_case_headers: false,
422424
http2: false,
423425
}
424426
}
@@ -435,6 +437,11 @@ impl Builder {
435437
self
436438
}
437439

440+
pub(super) fn h1_title_case_headers(&mut self, enabled: bool) -> &mut Builder {
441+
self.h1_title_case_headers = enabled;
442+
self
443+
}
444+
438445
/// Sets whether HTTP2 is required.
439446
///
440447
/// Default is false.
@@ -550,6 +557,9 @@ where
550557
if !self.builder.h1_writev {
551558
conn.set_write_strategy_flatten();
552559
}
560+
if self.builder.h1_title_case_headers {
561+
conn.set_title_case_headers();
562+
}
553563
let cd = proto::h1::dispatch::Client::new(rx);
554564
let dispatch = proto::h1::Dispatcher::new(cd, conn);
555565
Either::A(dispatch)

‎src/client/mod.rs

+18
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ pub struct Client<C, B = Body> {
3434
connector: Arc<C>,
3535
executor: Exec,
3636
h1_writev: bool,
37+
h1_title_case_headers: bool,
3738
pool: Pool<PoolClient<B>>,
3839
retry_canceled_requests: bool,
3940
set_host: bool,
@@ -186,6 +187,7 @@ where C: Connect + Sync + 'static,
186187
let executor = self.executor.clone();
187188
let pool = self.pool.clone();
188189
let h1_writev = self.h1_writev;
190+
let h1_title_case_headers = self.h1_title_case_headers;
189191
let connector = self.connector.clone();
190192
let dst = Destination {
191193
uri: url,
@@ -197,6 +199,7 @@ where C: Connect + Sync + 'static,
197199
.and_then(move |(io, connected)| {
198200
conn::Builder::new()
199201
.h1_writev(h1_writev)
202+
.h1_title_case_headers(h1_title_case_headers)
200203
.http2_only(pool_key.1 == Ver::Http2)
201204
.handshake_no_upgrades(io)
202205
.and_then(move |(tx, conn)| {
@@ -335,6 +338,7 @@ impl<C, B> Clone for Client<C, B> {
335338
connector: self.connector.clone(),
336339
executor: self.executor.clone(),
337340
h1_writev: self.h1_writev,
341+
h1_title_case_headers: self.h1_title_case_headers,
338342
pool: self.pool.clone(),
339343
retry_canceled_requests: self.retry_canceled_requests,
340344
set_host: self.set_host,
@@ -526,6 +530,7 @@ pub struct Builder {
526530
keep_alive: bool,
527531
keep_alive_timeout: Option<Duration>,
528532
h1_writev: bool,
533+
h1_title_case_headers: bool,
529534
//TODO: make use of max_idle config
530535
max_idle: usize,
531536
retry_canceled_requests: bool,
@@ -540,6 +545,7 @@ impl Default for Builder {
540545
keep_alive: true,
541546
keep_alive_timeout: Some(Duration::from_secs(90)),
542547
h1_writev: true,
548+
h1_title_case_headers: false,
543549
max_idle: 5,
544550
retry_canceled_requests: true,
545551
set_host: true,
@@ -583,6 +589,17 @@ impl Builder {
583589
self
584590
}
585591

592+
/// Set whether HTTP/1 connections will write header names as title case at
593+
/// the socket level.
594+
///
595+
/// Note that this setting does not affect HTTP/2.
596+
///
597+
/// Default is false.
598+
pub fn http1_title_case_headers(&mut self, val: bool) -> &mut Self {
599+
self.h1_title_case_headers = val;
600+
self
601+
}
602+
586603
/// Set whether the connection **must** use HTTP/2.
587604
///
588605
/// Note that setting this to true prevents HTTP/1 from being allowed.
@@ -662,6 +679,7 @@ impl Builder {
662679
connector: Arc::new(connector),
663680
executor: self.exec.clone(),
664681
h1_writev: self.h1_writev,
682+
h1_title_case_headers: self.h1_title_case_headers,
665683
pool: Pool::new(self.keep_alive, self.keep_alive_timeout),
666684
retry_canceled_requests: self.retry_canceled_requests,
667685
set_host: self.set_host,

‎src/proto/h1/conn.rs

+8-1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ where I: AsyncRead + AsyncWrite,
5050
error: None,
5151
keep_alive: KA::Busy,
5252
method: None,
53+
title_case_headers: false,
5354
read_task: None,
5455
reading: Reading::Init,
5556
writing: Writing::Init,
@@ -73,6 +74,10 @@ where I: AsyncRead + AsyncWrite,
7374
self.io.set_write_strategy_flatten();
7475
}
7576

77+
pub fn set_title_case_headers(&mut self) {
78+
self.state.title_case_headers = true;
79+
}
80+
7681
pub fn into_inner(self) -> (I, Bytes) {
7782
self.io.into_inner()
7883
}
@@ -430,7 +435,7 @@ where I: AsyncRead + AsyncWrite,
430435
self.enforce_version(&mut head);
431436

432437
let buf = self.io.write_buf_mut();
433-
self.state.writing = match T::encode(head, body, &mut self.state.method, buf) {
438+
self.state.writing = match T::encode(head, body, &mut self.state.method, self.state.title_case_headers, buf) {
434439
Ok(encoder) => {
435440
if !encoder.is_eof() {
436441
Writing::Body(encoder)
@@ -620,6 +625,7 @@ struct State {
620625
error: Option<::Error>,
621626
keep_alive: KA,
622627
method: Option<Method>,
628+
title_case_headers: bool,
623629
read_task: Option<Task>,
624630
reading: Reading,
625631
writing: Writing,
@@ -649,6 +655,7 @@ impl fmt::Debug for State {
649655
.field("keep_alive", &self.keep_alive)
650656
.field("error", &self.error)
651657
//.field("method", &self.method)
658+
//.field("title_case_headers", &self.title_case_headers)
652659
.field("read_task", &self.read_task)
653660
.finish()
654661
}

‎src/proto/h1/role.rs

+61-2
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ where
126126
mut head: MessageHead<Self::Outgoing>,
127127
body: Option<BodyLength>,
128128
method: &mut Option<Method>,
129+
_title_case_headers: bool,
129130
dst: &mut Vec<u8>,
130131
) -> ::Result<Encoder> {
131132
trace!("Server::encode body={:?}, method={:?}", body, method);
@@ -367,6 +368,7 @@ where
367368
mut head: MessageHead<Self::Outgoing>,
368369
body: Option<BodyLength>,
369370
method: &mut Option<Method>,
371+
title_case_headers: bool,
370372
dst: &mut Vec<u8>,
371373
) -> ::Result<Encoder> {
372374
trace!("Client::encode body={:?}, method={:?}", body, method);
@@ -391,7 +393,11 @@ where
391393
}
392394
extend(dst, b"\r\n");
393395

394-
write_headers(&head.headers, dst);
396+
if title_case_headers {
397+
write_headers_title_case(&head.headers, dst);
398+
} else {
399+
write_headers(&head.headers, dst);
400+
}
395401
extend(dst, b"\r\n");
396402

397403
Ok(body)
@@ -635,6 +641,44 @@ impl<'a> Iterator for HeadersAsBytesIter<'a> {
635641
}
636642
}
637643

644+
// Write header names as title case. The header name is assumed to be ASCII,
645+
// therefore it is trivial to convert an ASCII character from lowercase to
646+
// uppercase. It is as simple as XORing the lowercase character byte with
647+
// space.
648+
fn title_case(dst: &mut Vec<u8>, name: &[u8]) {
649+
dst.reserve(name.len());
650+
651+
let mut iter = name.iter();
652+
653+
// Uppercase the first character
654+
if let Some(c) = iter.next() {
655+
if *c >= b'a' && *c <= b'z' {
656+
dst.push(*c ^ b' ');
657+
}
658+
}
659+
660+
while let Some(c) = iter.next() {
661+
dst.push(*c);
662+
663+
if *c == b'-' {
664+
if let Some(c) = iter.next() {
665+
if *c >= b'a' && *c <= b'z' {
666+
dst.push(*c ^ b' ');
667+
}
668+
}
669+
}
670+
}
671+
}
672+
673+
fn write_headers_title_case(headers: &HeaderMap, dst: &mut Vec<u8>) {
674+
for (name, value) in headers {
675+
title_case(dst, name.as_str().as_bytes());
676+
extend(dst, b": ");
677+
extend(dst, value.as_bytes());
678+
extend(dst, b"\r\n");
679+
}
680+
}
681+
638682
fn write_headers(headers: &HeaderMap, dst: &mut Vec<u8>) {
639683
for (name, value) in headers {
640684
extend(dst, name.as_str().as_bytes());
@@ -857,6 +901,21 @@ mod tests {
857901
Client::decoder(&head, method).unwrap_err();
858902
}
859903

904+
#[test]
905+
fn test_client_request_encode_title_case() {
906+
use http::header::HeaderValue;
907+
use proto::BodyLength;
908+
909+
let mut head = MessageHead::default();
910+
head.headers.insert("content-length", HeaderValue::from_static("10"));
911+
head.headers.insert("content-type", HeaderValue::from_static("application/json"));
912+
913+
let mut vec = Vec::new();
914+
Client::encode(head, Some(BodyLength::Known(10)), &mut None, true, &mut vec).unwrap();
915+
916+
assert_eq!(vec, b"GET / HTTP/1.1\r\nContent-Length: 10\r\nContent-Type: application/json\r\n\r\n".to_vec());
917+
}
918+
860919
#[cfg(feature = "nightly")]
861920
use test::Bencher;
862921

@@ -914,7 +973,7 @@ mod tests {
914973

915974
b.iter(|| {
916975
let mut vec = Vec::new();
917-
Server::encode(head.clone(), Some(BodyLength::Known(10)), &mut None, &mut vec).unwrap();
976+
Server::encode(head.clone(), Some(BodyLength::Known(10)), &mut None, false, &mut vec).unwrap();
918977
assert_eq!(vec.len(), len);
919978
::test::black_box(vec);
920979
})

‎src/proto/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ pub(crate) trait Http1Transaction {
7272
head: MessageHead<Self::Outgoing>,
7373
body: Option<BodyLength>,
7474
method: &mut Option<Method>,
75+
title_case_headers: bool,
7576
dst: &mut Vec<u8>,
7677
) -> ::Result<h1::Encoder>;
7778
fn on_error(err: &::Error) -> Option<MessageHead<Self::Outgoing>>;

0 commit comments

Comments
 (0)