Skip to content

Commit 800384f

Browse files
committed
fix(server): [WIP] draft 2427 fixes (update 2!)
there are some tests fixes now, but they may be controversial. I was moved to take advantage of the fact that Body::empty() provides no SizeHint to encode(). But, encode would still take a None and write `content-length: 0`. For the moment, I've changed that. But, this may be an unexpected change for some. I'd expect most services do not go out of their way to differentiate between GET and HEAD requests. This works out fine, in that any determinably sized body ends up creating a Content-Length header for either request. As written in this draft, a 304 response with Body::empty() will have no Content-Length. But, if anyone had written one using something like `"".into()`, this change will now add `content-length: 0`.
1 parent 4258705 commit 800384f

File tree

4 files changed

+111
-67
lines changed

4 files changed

+111
-67
lines changed

src/body/body.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ impl From<Bytes> for Body {
480480
#[inline]
481481
fn from(chunk: Bytes) -> Body {
482482
if chunk.is_empty() {
483-
Body::empty()
483+
Body::new(Kind::Once(Some(Bytes::new()))) // drop chunk ASAP
484484
} else {
485485
Body::new(Kind::Once(Some(chunk)))
486486
}

src/proto/h1/role.rs

+78-59
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ impl Http1Transaction for Server {
385385
}};
386386
}
387387

388-
'headers: for (opt_name, value) in msg.head.headers.drain() {
388+
for (opt_name, value) in msg.head.headers.drain() {
389389
if let Some(n) = opt_name {
390390
cur_name = Some(n);
391391
handle_is_name_written!();
@@ -399,6 +399,14 @@ impl Http1Transaction for Server {
399399
rewind(dst);
400400
return Err(crate::Error::new_user_header());
401401
}
402+
// Can this response even contain Content-Length?
403+
if !Server::can_have_body(msg.req_method, msg.head.subject) {
404+
warn!(
405+
"illegal content-length ({:?}) seen for {:?} response to {:?} request",
406+
value, msg.head.subject, msg.req_method
407+
);
408+
continue;
409+
}
402410
match msg.body {
403411
Some(BodyLength::Known(known_len)) => {
404412
// The HttpBody claims to know a length, and
@@ -421,13 +429,15 @@ impl Http1Transaction for Server {
421429
}
422430

423431
if !is_name_written {
424-
encoder = Encoder::length(known_len);
432+
if !Server::imply_body_only(msg.req_method, msg.head.subject) {
433+
encoder = Encoder::length(known_len);
434+
}
425435
extend(dst, b"content-length: ");
426436
extend(dst, value.as_bytes());
427437
wrote_len = true;
428438
is_name_written = true;
429439
}
430-
continue 'headers;
440+
continue;
431441
}
432442
Some(BodyLength::Unknown) => {
433443
// The HttpBody impl didn't know how long the
@@ -446,16 +456,18 @@ impl Http1Transaction for Server {
446456
return Err(crate::Error::new_user_header());
447457
}
448458
debug_assert!(is_name_written);
449-
continue 'headers;
459+
continue;
450460
} else {
451461
// we haven't written content-length yet!
452-
encoder = Encoder::length(len);
462+
if !Server::imply_body_only(msg.req_method, msg.head.subject) {
463+
encoder = Encoder::length(len);
464+
}
453465
extend(dst, b"content-length: ");
454466
extend(dst, value.as_bytes());
455467
wrote_len = true;
456468
is_name_written = true;
457469
prev_con_len = Some(len);
458-
continue 'headers;
470+
continue;
459471
}
460472
} else {
461473
warn!("illegal Content-Length value: {:?}", value);
@@ -464,13 +476,13 @@ impl Http1Transaction for Server {
464476
}
465477
}
466478
None => {
467-
// We have no body to actually send,
468-
// but the headers claim a content-length.
469-
// There's only 2 ways this makes sense:
479+
// We have no body to actually send, but the headers claim a
480+
// content-length. There are 3 ways this makes sense:
470481
//
471482
// - The header says the length is `0`.
483+
// - The response is NOT_MODIFIED
472484
// - This is a response to a `HEAD` request.
473-
if msg.req_method == &Some(Method::HEAD) {
485+
if Server::imply_body_only(msg.req_method, msg.head.subject) {
474486
debug_assert_eq!(encoder, Encoder::length(0));
475487
} else {
476488
if value.as_bytes() != b"0" {
@@ -479,7 +491,7 @@ impl Http1Transaction for Server {
479491
value
480492
);
481493
}
482-
continue 'headers;
494+
continue;
483495
}
484496
}
485497
}
@@ -492,9 +504,13 @@ impl Http1Transaction for Server {
492504
return Err(crate::Error::new_user_header());
493505
}
494506
// check that we actually can send a chunked body...
495-
if msg.head.version == Version::HTTP_10
496-
|| !Server::can_chunked(msg.req_method, msg.head.subject)
497-
{
507+
if msg.head.version == Version::HTTP_10 {
508+
continue;
509+
} else if !Server::can_have_body(msg.req_method, msg.head.subject) {
510+
warn!(
511+
"illegal transfer-encoding ({:?}) seen for {:?} response to {:?} request",
512+
value, msg.head.subject, msg.req_method
513+
);
498514
continue;
499515
}
500516
wrote_len = true;
@@ -503,15 +519,17 @@ impl Http1Transaction for Server {
503519
must_write_chunked = !headers::is_chunked_(&value);
504520

505521
if !is_name_written {
506-
encoder = Encoder::chunked();
522+
if !Server::imply_body_only(msg.req_method, msg.head.subject) {
523+
encoder = Encoder::chunked();
524+
}
507525
is_name_written = true;
508526
extend(dst, b"transfer-encoding: ");
509527
extend(dst, value.as_bytes());
510528
} else {
511529
extend(dst, b", ");
512530
extend(dst, value.as_bytes());
513531
}
514-
continue 'headers;
532+
continue;
515533
}
516534
header::CONNECTION => {
517535
if !is_last && headers::connection_close(&value) {
@@ -525,7 +543,7 @@ impl Http1Transaction for Server {
525543
extend(dst, b", ");
526544
extend(dst, value.as_bytes());
527545
}
528-
continue 'headers;
546+
continue;
529547
}
530548
header::DATE => {
531549
wrote_date = true;
@@ -549,44 +567,50 @@ impl Http1Transaction for Server {
549567

550568
handle_is_name_written!();
551569

552-
if !wrote_len {
553-
encoder = match msg.body {
554-
Some(BodyLength::Unknown) => {
555-
if msg.head.version == Version::HTTP_10
556-
|| !Server::can_chunked(msg.req_method, msg.head.subject)
557-
{
558-
Encoder::close_delimited()
559-
} else {
560-
extend(dst, b"transfer-encoding: chunked\r\n");
561-
Encoder::chunked()
562-
}
570+
// We should provide either a Content-Length or Transfer-Encoding header based on what we
571+
// know about the Body, unless:
572+
// - the request was HEAD and the body is Body::empty(), because a service may simply not
573+
// provide generate the real body for a HEAD request.
574+
// - the response is a 304, like a HEAD, may imply things about a particular body, but a
575+
// service probably did not provide the full body in its Response.
576+
// - the response must neither contain nor imply a body.
577+
if !wrote_len && Server::can_have_body(msg.req_method, msg.head.subject) {
578+
match msg.body {
579+
Some(BodyLength::Known(0)) => extend(dst, b"content-length: 0\r\n"),
580+
Some(BodyLength::Known(len)) => {
581+
extend(dst, b"content-length: ");
582+
let _ = ::itoa::write(&mut dst, len);
583+
extend(dst, b"\r\n");
563584
}
564-
None | Some(BodyLength::Known(0)) => {
565-
if msg.head.subject != StatusCode::NOT_MODIFIED {
585+
None => {
586+
if !Server::imply_body_only(msg.req_method, msg.head.subject) {
566587
extend(dst, b"content-length: 0\r\n");
567588
}
568-
Encoder::length(0)
569589
}
570-
Some(BodyLength::Known(len)) => {
571-
if msg.head.subject == StatusCode::NOT_MODIFIED {
572-
Encoder::length(0)
573-
} else {
574-
extend(dst, b"content-length: ");
575-
let _ = ::itoa::write(&mut dst, len);
576-
extend(dst, b"\r\n");
577-
Encoder::length(len)
590+
Some(BodyLength::Unknown) => {
591+
if msg.head.version != Version::HTTP_10 {
592+
extend(dst, b"transfer-encoding: chunked\r\n");
578593
}
579594
}
580595
};
596+
597+
if !Server::imply_body_only(msg.req_method, msg.head.subject) {
598+
encoder = match msg.body {
599+
Some(BodyLength::Unknown) if msg.head.version == Version::HTTP_10 => {
600+
Encoder::close_delimited()
601+
}
602+
Some(BodyLength::Unknown) => Encoder::chunked(),
603+
Some(BodyLength::Known(len)) => Encoder::length(len),
604+
None => Encoder::length(0),
605+
};
606+
}
581607
}
582608

583-
if !Server::can_have_body(msg.req_method, msg.head.subject) {
584-
trace!(
585-
"server body forced to 0; method={:?}, status={:?}",
586-
msg.req_method,
587-
msg.head.subject
588-
);
589-
encoder = Encoder::length(0);
609+
#[cfg(debug_assertions)]
610+
if Server::imply_body_only(msg.req_method, msg.head.subject)
611+
|| !Server::can_have_body(msg.req_method, msg.head.subject)
612+
{
613+
assert_eq!(encoder, Encoder::length(0));
590614
}
591615

592616
// cached date is much faster than formatting every request
@@ -631,24 +655,19 @@ impl Http1Transaction for Server {
631655
#[cfg(feature = "server")]
632656
impl Server {
633657
fn can_have_body(method: &Option<Method>, status: StatusCode) -> bool {
634-
Server::can_chunked(method, status)
635-
}
636-
637-
fn can_chunked(method: &Option<Method>, status: StatusCode) -> bool {
638-
if method == &Some(Method::HEAD) || method == &Some(Method::CONNECT) && status.is_success()
658+
if method == &Some(Method::CONNECT) && status.is_success()
659+
|| status == StatusCode::NO_CONTENT
660+
|| status.is_informational()
639661
{
640662
false
641663
} else {
642-
match status {
643-
// TODO: support for 1xx codes needs improvement everywhere
644-
// would be 100...199 => false
645-
StatusCode::SWITCHING_PROTOCOLS
646-
| StatusCode::NO_CONTENT
647-
| StatusCode::NOT_MODIFIED => false,
648-
_ => true,
649-
}
664+
true
650665
}
651666
}
667+
668+
fn imply_body_only(method: &Option<Method>, status: StatusCode) -> bool {
669+
method == &Some(Method::HEAD) || status == StatusCode::NOT_MODIFIED
670+
}
652671
}
653672

654673
#[cfg(feature = "client")]

tests/client.rs

+19
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,25 @@ test! {
359359
expected: "GET / HTTP/1.1\r\nhost: {addr}\r\n\r\n",
360360
reply: REPLY_OK,
361361

362+
client:
363+
request: {
364+
method: GET,
365+
url: "http://{addr}/",
366+
// body is Body::empty
367+
},
368+
response:
369+
status: OK,
370+
headers: {},
371+
body: None,
372+
}
373+
374+
test! {
375+
name: client_get_req_body_explicitly_empty,
376+
377+
server:
378+
expected: "GET / HTTP/1.1\r\nhost: {addr}\r\ncontent-length: 0\r\n\r\n",
379+
reply: REPLY_OK,
380+
362381
client:
363382
request: {
364383
method: GET,

tests/server.rs

+13-7
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ fn response_does_not_set_chunked_if_body_not_allowed() {
515515
let server = serve();
516516
server
517517
.reply()
518-
.status(hyper::StatusCode::NOT_MODIFIED)
518+
.status(hyper::StatusCode::NO_CONTENT)
519519
.header("transfer-encoding", "chunked");
520520
let mut req = connect(server.addr());
521521
req.write_all(
@@ -534,7 +534,7 @@ fn response_does_not_set_chunked_if_body_not_allowed() {
534534
assert!(!response.contains("transfer-encoding"));
535535

536536
let mut lines = response.lines();
537-
assert_eq!(lines.next(), Some("HTTP/1.1 304 Not Modified"));
537+
assert_eq!(lines.next(), Some("HTTP/1.1 204 No Content"));
538538

539539
// no body or 0\r\n\r\n
540540
let mut lines = lines.skip_while(|line| !line.is_empty());
@@ -1775,7 +1775,7 @@ async fn http2_service_poll_ready_error_sends_goaway() {
17751775
}
17761776

17771777
#[test]
1778-
fn skips_content_length_for_304_responses() {
1778+
fn keeps_content_length_and_drops_body_for_304_responses() {
17791779
let server = serve();
17801780
server
17811781
.reply()
@@ -1794,15 +1794,21 @@ fn skips_content_length_for_304_responses() {
17941794

17951795
let mut response = String::new();
17961796
req.read_to_string(&mut response).unwrap();
1797-
assert!(!response.contains("content-length:"));
1797+
assert!(response.contains("content-length: 3"));
1798+
let mut lines = response.lines();
1799+
assert_eq!(lines.next(), Some("HTTP/1.1 304 Not Modified"));
1800+
1801+
let mut lines = lines.skip_while(|line| !line.is_empty());
1802+
assert_eq!(lines.next(), Some(""));
1803+
assert_eq!(lines.next(), None);
17981804
}
17991805

18001806
#[test]
1801-
fn skips_content_length_and_body_for_304_responses() {
1807+
fn skips_content_length_and_body_for_204_response() {
18021808
let server = serve();
18031809
server
18041810
.reply()
1805-
.status(hyper::StatusCode::NOT_MODIFIED)
1811+
.status(hyper::StatusCode::NO_CONTENT)
18061812
.body("foo");
18071813
let mut req = connect(server.addr());
18081814
req.write_all(
@@ -1819,7 +1825,7 @@ fn skips_content_length_and_body_for_304_responses() {
18191825
req.read_to_string(&mut response).unwrap();
18201826
assert!(!response.contains("content-length:"));
18211827
let mut lines = response.lines();
1822-
assert_eq!(lines.next(), Some("HTTP/1.1 304 Not Modified"));
1828+
assert_eq!(lines.next(), Some("HTTP/1.1 204 No Content"));
18231829

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

0 commit comments

Comments
 (0)