Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/proto/h1/role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,12 @@ impl Http1Transaction for Server {
msg.req_method
);

// A 205 response cannot contain content (RFC 9110 Section 15.3.6),
// so drop the body and let the framing code send a zero length.
if msg.head.subject == StatusCode::RESET_CONTENT {
msg.body = None;
}

let mut wrote_len = false;

// hyper currently doesn't support returning 1xx status codes as a Response
Expand Down Expand Up @@ -531,7 +537,10 @@ impl Server {
{
false
} else {
!matches!(status, StatusCode::NO_CONTENT | StatusCode::NOT_MODIFIED)
!matches!(
status,
StatusCode::NO_CONTENT | StatusCode::RESET_CONTENT | StatusCode::NOT_MODIFIED
)
}
}

Expand Down
39 changes: 38 additions & 1 deletion src/proto/h2/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use bytes::Bytes;
use futures_core::ready;
use h2::server::{Connection, Handshake, SendResponse};
use h2::{Reason, RecvStream};
use http::{Method, Request};
use http::{Method, Request, StatusCode};
use pin_project_lite::pin_project;

use super::{ping, PipeToSendStream, SendBuf};
Expand Down Expand Up @@ -273,6 +273,7 @@ where
ping.record_non_data();

let is_connect = req.method() == Method::CONNECT;
let is_head = req.method() == Method::HEAD;
let (mut parts, stream) = req.into_parts();
let (mut req, connect_parts) = if !is_connect {
(
Expand Down Expand Up @@ -309,6 +310,7 @@ where
service.call(req),
connect_parts,
respond,
is_head,
self.date_header,
exec.clone(),
);
Expand Down Expand Up @@ -367,6 +369,7 @@ pin_project! {
reply: SendResponse<SendBuf<B::Data>>,
#[pin]
state: H2StreamState<F, B>,
is_head: bool,
date_header: bool,
exec: E,
}
Expand Down Expand Up @@ -404,12 +407,14 @@ where
fut: F,
connect_parts: Option<ConnectParts>,
respond: SendResponse<SendBuf<B::Data>>,
is_head: bool,
date_header: bool,
exec: E,
) -> H2Stream<F, B, E> {
H2Stream {
reply: respond,
state: H2StreamState::Service { fut, connect_parts },
is_head,
date_header,
exec,
}
Expand Down Expand Up @@ -511,6 +516,38 @@ where
}
}

// Responses to HEAD requests, as well as 204, 205, and 304
// responses, never contain content (RFC 9110 Section 6.4.1
// and Section 15.3.6).
let status = res.status();
if *me.is_head
|| matches!(
status,
StatusCode::NO_CONTENT
| StatusCode::RESET_CONTENT
| StatusCode::NOT_MODIFIED
)
{
if *me.is_head {
// A HEAD response may include the Content-Length
// that a GET response would have sent.
if !body.is_end_stream() {
if let Some(len) = body.size_hint().exact() {
headers::set_content_length_if_missing(res.headers_mut(), len);
}
}
} else if matches!(
status,
StatusCode::NO_CONTENT | StatusCode::RESET_CONTENT
) {
// 204 cannot include Content-Length (RFC 9113
// Section 8.1) and a 205 response has no content.
res.headers_mut().remove(::http::header::CONTENT_LENGTH);
}
reply!(me, res, true);
return Poll::Ready(Ok(()));
}

if !body.is_end_stream() {
// automatically set Content-Length from body...
if let Some(len) = body.size_hint().exact() {
Expand Down
113 changes: 113 additions & 0 deletions tests/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,89 @@ mod response_body_lengths {
assert_eq!(res.headers().get("content-length"), None);
assert_eq!(res.body().size_hint().exact(), Some(0));
}

#[tokio::test]
async fn http2_skips_body_for_204_responses() {
let server = serve_opts().http2().serve();
let addr_str = format!("http://{}", server.addr());
server
.reply()
.status(hyper::StatusCode::NO_CONTENT)
.body("Hello, World!");

let client = TestClient::new().http2_only();
let uri = addr_str
.parse::<hyper::Uri>()
.expect("server addr should parse");

let res = client.get(uri).await.unwrap();
assert_eq!(res.status(), hyper::StatusCode::NO_CONTENT);
assert_eq!(res.headers().get("content-length"), None);
assert!(res
.into_body()
.collect()
.await
.unwrap()
.to_bytes()
.is_empty());
}

#[tokio::test]
async fn http2_skips_body_for_205_responses() {
let server = serve_opts().http2().serve();
let addr_str = format!("http://{}", server.addr());
server
.reply()
.status(hyper::StatusCode::RESET_CONTENT)
.body("Hello, World!");

let client = TestClient::new().http2_only();
let uri = addr_str
.parse::<hyper::Uri>()
.expect("server addr should parse");

let res = client.get(uri).await.unwrap();
assert_eq!(res.status(), hyper::StatusCode::RESET_CONTENT);
assert_eq!(res.headers().get("content-length"), None);
assert!(res
.into_body()
.collect()
.await
.unwrap()
.to_bytes()
.is_empty());
}

#[tokio::test]
async fn http2_skips_body_for_head_responses() {
let server = serve_opts().http2().serve();
let addr_str = format!("http://{}", server.addr());
server.reply().body("Hello, World!");

let client = TestClient::new().http2_only();
let uri = addr_str
.parse::<hyper::Uri>()
.expect("server addr should parse");

let res = client
.request(
Request::builder()
.uri(uri)
.method(Method::HEAD)
.body(Empty::<Bytes>::new())
.unwrap(),
)
.await
.unwrap();
assert_eq!(res.headers().get("content-length").unwrap(), "13");
assert!(res
.into_body()
.collect()
.await
.unwrap()
.to_bytes()
.is_empty());
}
}

#[test]
Expand Down Expand Up @@ -3067,6 +3150,36 @@ fn skips_content_length_and_body_for_304_responses() {
assert_eq!(lines.next(), None);
}

#[test]
fn skips_body_for_205_responses() {
let server = serve();
server
.reply()
.status(hyper::StatusCode::RESET_CONTENT)
.body("foo");
let mut req = connect(server.addr());
req.write_all(
b"\
GET / HTTP/1.1\r\n\
Host: example.domain\r\n\
Connection: close\r\n\
\r\n\
",
)
.unwrap();

let mut response = String::new();
req.read_to_string(&mut response).unwrap();
assert!(response.starts_with("HTTP/1.1 205 Reset Content\r\n"));
assert!(response.contains("content-length: 0\r\n"));
let mut lines = response.lines();
lines.next();

let mut lines = lines.skip_while(|line| !line.is_empty());
assert_eq!(lines.next(), Some(""));
assert_eq!(lines.next(), None);
}

#[test]
fn no_implicit_zero_content_length_for_head_responses() {
let server = serve();
Expand Down
Loading