Skip to content

Commit

Permalink
feat(server): add Builder::auto_date_header(bool) to allow disablin…
Browse files Browse the repository at this point in the history
…g Date headers

The default is enabled `true`. The method exists on both HTTP/1 and HTTP/2 builders.
  • Loading branch information
edwardwc committed Apr 29, 2024
1 parent a3269f7 commit 721785e
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 6 deletions.
6 changes: 6 additions & 0 deletions src/proto/h1/conn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ where
#[cfg(feature = "server")]
h1_header_read_timeout_running: false,
#[cfg(feature = "server")]
date_header: true,
#[cfg(feature = "server")]
timer: Time::Empty,
preserve_header_case: false,
#[cfg(feature = "ffi")]
Expand Down Expand Up @@ -564,6 +566,8 @@ where
keep_alive: self.state.wants_keep_alive(),
req_method: &mut self.state.method,
title_case_headers: self.state.title_case_headers,
#[cfg(feature = "server")]
date_header: self.state.date_header,
},
buf,
) {
Expand Down Expand Up @@ -859,6 +863,8 @@ struct State {
#[cfg(feature = "server")]
h1_header_read_timeout_running: bool,
#[cfg(feature = "server")]
date_header: bool,
#[cfg(feature = "server")]
timer: Time,
preserve_header_case: bool,
#[cfg(feature = "ffi")]
Expand Down
2 changes: 2 additions & 0 deletions src/proto/h1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ pub(crate) struct Encode<'a, T> {
keep_alive: bool,
req_method: &'a mut Option<Method>,
title_case_headers: bool,
#[cfg(feature = "server")]
date_header: bool,
}

/// Extra flags that a request "wants", like expect-continue or upgrades.
Expand Down
48 changes: 47 additions & 1 deletion src/proto/h1/role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,8 @@ impl Server {
}

// cached date is much faster than formatting every request
if !wrote_date {
// don't force the write if disabled
if !wrote_date && msg.date_header {
dst.reserve(date::DATE_VALUE_LENGTH + 8);
header_name_writer.write_header_name_with_colon(dst, "date: ", header::DATE);
date::extend(dst);
Expand Down Expand Up @@ -2503,6 +2504,7 @@ mod tests {
keep_alive: true,
req_method: &mut None,
title_case_headers: true,
date_header: true,
},
&mut vec,
)
Expand Down Expand Up @@ -2534,6 +2536,7 @@ mod tests {
keep_alive: true,
req_method: &mut None,
title_case_headers: false,
date_header: true,
},
&mut vec,
)
Expand Down Expand Up @@ -2568,6 +2571,7 @@ mod tests {
keep_alive: true,
req_method: &mut None,
title_case_headers: true,
date_header: true,
},
&mut vec,
)
Expand All @@ -2592,6 +2596,7 @@ mod tests {
keep_alive: true,
req_method: &mut Some(Method::CONNECT),
title_case_headers: false,
date_header: true,
},
&mut vec,
)
Expand Down Expand Up @@ -2621,6 +2626,7 @@ mod tests {
keep_alive: true,
req_method: &mut None,
title_case_headers: true,
date_header: true,
},
&mut vec,
)
Expand Down Expand Up @@ -2655,6 +2661,7 @@ mod tests {
keep_alive: true,
req_method: &mut None,
title_case_headers: false,
date_header: true,
},
&mut vec,
)
Expand Down Expand Up @@ -2689,17 +2696,54 @@ mod tests {
keep_alive: true,
req_method: &mut None,
title_case_headers: true,
date_header: true,
},
&mut vec,
)
.unwrap();

// this will also test that the date does exist
let expected_response =
b"HTTP/1.1 200 OK\r\nCONTENT-LENGTH: 10\r\nContent-Type: application/json\r\nDate: ";

assert_eq!(&vec[..expected_response.len()], &expected_response[..]);
}

#[test]
fn test_disabled_date_header() {
use crate::proto::BodyLength;
use http::header::{HeaderValue, CONTENT_LENGTH};

let mut head = MessageHead::default();
head.headers
.insert("content-length", HeaderValue::from_static("10"));
head.headers
.insert("content-type", HeaderValue::from_static("application/json"));

let mut orig_headers = HeaderCaseMap::default();
orig_headers.insert(CONTENT_LENGTH, "CONTENT-LENGTH".into());
head.extensions.insert(orig_headers);

let mut vec = Vec::new();
Server::encode(
Encode {
head: &mut head,
body: Some(BodyLength::Known(10)),
keep_alive: true,
req_method: &mut None,
title_case_headers: true,
date_header: false,
},
&mut vec,
)
.unwrap();

let expected_response =
b"HTTP/1.1 200 OK\r\nCONTENT-LENGTH: 10\r\nContent-Type: application/json\r\n\r\n";

assert_eq!(&vec, &expected_response);
}

#[test]
fn parse_header_htabs() {
let mut bytes = BytesMut::from("HTTP/1.1 200 OK\r\nserver: hello\tworld\r\n\r\n");
Expand Down Expand Up @@ -3037,6 +3081,7 @@ mod tests {
keep_alive: true,
req_method: &mut Some(Method::GET),
title_case_headers: false,
date_header: true,
},
&mut vec,
)
Expand Down Expand Up @@ -3065,6 +3110,7 @@ mod tests {
keep_alive: true,
req_method: &mut Some(Method::GET),
title_case_headers: false,
date_header: true,
},
&mut vec,
)
Expand Down
27 changes: 22 additions & 5 deletions src/proto/h2/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pub(crate) struct Config {
pub(crate) keep_alive_timeout: Duration,
pub(crate) max_send_buffer_size: usize,
pub(crate) max_header_list_size: u32,
pub(crate) date_header: bool,
}

impl Default for Config {
Expand All @@ -72,6 +73,7 @@ impl Default for Config {
keep_alive_timeout: Duration::from_secs(20),
max_send_buffer_size: DEFAULT_MAX_SEND_BUF_SIZE,
max_header_list_size: DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE,
date_header: true,
}
}
}
Expand All @@ -86,6 +88,7 @@ pin_project! {
timer: Time,
service: S,
state: State<T, B>,
date_header: bool,
}
}

Expand All @@ -108,6 +111,7 @@ where
ping: Option<(ping::Recorder, ping::Ponger)>,
conn: Connection<Compat<T>, SendBuf<B::Data>>,
closing: Option<crate::Error>,
date_header: bool,
}

impl<T, S, B, E> Server<T, S, B, E>
Expand Down Expand Up @@ -167,6 +171,7 @@ where
hs: handshake,
},
service,
date_header: config.date_header,
}
}

Expand Down Expand Up @@ -219,6 +224,7 @@ where
ping,
conn,
closing: None,
date_header: me.date_header,
})
}
State::Serving(ref mut srv) => {
Expand Down Expand Up @@ -302,7 +308,13 @@ where
req.extensions_mut().insert(Protocol::from_inner(protocol));
}

let fut = H2Stream::new(service.call(req), connect_parts, respond);
let fut = H2Stream::new(
service.call(req),
connect_parts,
respond,
self.date_header,
);

exec.execute_h2stream(fut);
}
Some(Err(e)) => {
Expand Down Expand Up @@ -357,6 +369,7 @@ pin_project! {
reply: SendResponse<SendBuf<B::Data>>,
#[pin]
state: H2StreamState<F, B>,
date_header: bool,
}
}

Expand Down Expand Up @@ -392,10 +405,12 @@ where
fut: F,
connect_parts: Option<ConnectParts>,
respond: SendResponse<SendBuf<B::Data>>,
date_header: bool,
) -> H2Stream<F, B> {
H2Stream {
reply: respond,
state: H2StreamState::Service { fut, connect_parts },
date_header,
}
}
}
Expand Down Expand Up @@ -454,10 +469,12 @@ where
let mut res = ::http::Response::from_parts(head, ());
super::strip_connection_headers(res.headers_mut(), false);

// set Date header if it isn't already set...
res.headers_mut()
.entry(::http::header::DATE)
.or_insert_with(date::update_and_header_value);
// set Date header if it isn't already set if instructed
if *me.date_header {
res.headers_mut()
.entry(::http::header::DATE)
.or_insert_with(date::update_and_header_value);
}

if let Some(connect_parts) = connect_parts.take() {
if res.status().is_success() {
Expand Down
12 changes: 12 additions & 0 deletions src/server/conn/http1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ pub struct Builder {
h1_writev: Option<bool>,
max_buf_size: Option<usize>,
pipeline_flush: bool,
date_header: bool,
}

/// Deconstructed parts of a `Connection`.
Expand Down Expand Up @@ -246,6 +247,7 @@ impl Builder {
h1_writev: None,
max_buf_size: None,
pipeline_flush: false,
date_header: true,
}
}
/// Set whether HTTP/1 connections should support half-closures.
Expand Down Expand Up @@ -359,6 +361,16 @@ impl Builder {
self
}

/// Set whether the `date` header should be included in HTTP responses.
///
/// Note that including the `date` header is recommended by RFC 7231.
///
/// Default is true.
pub fn auto_date_header(&mut self, enabled: bool) -> &mut Self {
self.date_header = enabled;
self
}

/// Aggregates flushes to better support pipelined responses.
///
/// Experimental, may have bugs.
Expand Down
10 changes: 10 additions & 0 deletions src/server/conn/http2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,16 @@ impl<E> Builder<E> {
self
}

/// Set whether the `date` header should be included in HTTP responses.
///
/// Note that including the `date` header is recommended by RFC 7231.
///
/// Default is true.
pub fn auto_date_header(&mut self, enabled: bool) -> &mut Self {
self.h2_builder.date_header = enabled;
self
}

/// Bind a connection together with a [`Service`](crate::service::Service).
///
/// This returns a Future that must be polled in order for HTTP to be
Expand Down
37 changes: 37 additions & 0 deletions tests/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2529,6 +2529,7 @@ async fn http2_keep_alive_detects_unresponsive_client() {
.timer(TokioTimer)
.keep_alive_interval(Duration::from_secs(1))
.keep_alive_timeout(Duration::from_secs(1))
.auto_date_header(true)
.serve_connection(socket, unreachable_service())
.await
.expect_err("serve_connection should error");
Expand Down Expand Up @@ -2569,6 +2570,42 @@ async fn http2_keep_alive_with_responsive_client() {
client.send_request(req).await.expect("client.send_request");
}

#[tokio::test]
async fn http2_check_date_header_disabled() {
let (listener, addr) = setup_tcp_listener();

tokio::spawn(async move {
let (socket, _) = listener.accept().await.expect("accept");
let socket = TokioIo::new(socket);

http2::Builder::new(TokioExecutor)
.timer(TokioTimer)
.keep_alive_interval(Duration::from_secs(1))
.auto_date_header(false)
.keep_alive_timeout(Duration::from_secs(1))
.serve_connection(socket, HelloWorld)
.await
.expect("serve_connection");
});

let tcp = TokioIo::new(connect_async(addr).await);
let (mut client, conn) = hyper::client::conn::http2::Builder::new(TokioExecutor)
.handshake(tcp)
.await
.expect("http handshake");

tokio::spawn(async move {
conn.await.expect("client conn");
});

TokioTimer.sleep(Duration::from_secs(4)).await;

let req = http::Request::new(Empty::<Bytes>::new());
let resp = client.send_request(req).await.expect("client.send_request");

assert!(resp.headers().get("Date").is_none());
}

fn is_ping_frame(buf: &[u8]) -> bool {
buf[3] == 6
}
Expand Down

1 comment on commit 721785e

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'end_to_end'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 2.

Benchmark suite Current: 721785e Previous: a3269f7 Ratio
http2_parallel_x10_req_10kb_100_chunks_adaptive_window 43196810 ns/iter (± 44211908) 7808819 ns/iter (± 147970) 5.53

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.