diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000000..32a9786fa1
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1 @@
+edition = "2018"
diff --git a/src/body/body.rs b/src/body/body.rs
index 1be2c1b0c2..fe9bf11dea 100644
--- a/src/body/body.rs
+++ b/src/body/body.rs
@@ -480,7 +480,7 @@ impl From<Bytes> for Body {
     #[inline]
     fn from(chunk: Bytes) -> Body {
         if chunk.is_empty() {
-            Body::empty()
+            Body::new(Kind::Once(Some(Bytes::new()))) // drop chunk ASAP
         } else {
             Body::new(Kind::Once(Some(chunk)))
         }
diff --git a/src/proto/h1/role.rs b/src/proto/h1/role.rs
index a9f2f0074f..144a06eed0 100644
--- a/src/proto/h1/role.rs
+++ b/src/proto/h1/role.rs
@@ -385,7 +385,7 @@ impl Http1Transaction for Server {
             }};
         }
 
-        'headers: for (opt_name, value) in msg.head.headers.drain() {
+        for (opt_name, value) in msg.head.headers.drain() {
             if let Some(n) = opt_name {
                 cur_name = Some(n);
                 handle_is_name_written!();
@@ -399,6 +399,14 @@ impl Http1Transaction for Server {
                         rewind(dst);
                         return Err(crate::Error::new_user_header());
                     }
+                    // Can this response even contain Content-Length?
+                    if !Server::can_have_body(msg.req_method, msg.head.subject) {
+                        warn!(
+                            "illegal content-length ({:?}) seen for {:?} response to {:?} request",
+                            value, msg.head.subject, msg.req_method
+                        );
+                        continue;
+                    }
                     match msg.body {
                         Some(BodyLength::Known(known_len)) => {
                             // The HttpBody claims to know a length, and
@@ -421,13 +429,15 @@ impl Http1Transaction for Server {
                             }
 
                             if !is_name_written {
-                                encoder = Encoder::length(known_len);
+                                if !Server::imply_body_only(msg.req_method, msg.head.subject) {
+                                    encoder = Encoder::length(known_len);
+                                }
                                 extend(dst, b"content-length: ");
                                 extend(dst, value.as_bytes());
                                 wrote_len = true;
                                 is_name_written = true;
                             }
-                            continue 'headers;
+                            continue;
                         }
                         Some(BodyLength::Unknown) => {
                             // The HttpBody impl didn't know how long the
@@ -446,16 +456,18 @@ impl Http1Transaction for Server {
                                         return Err(crate::Error::new_user_header());
                                     }
                                     debug_assert!(is_name_written);
-                                    continue 'headers;
+                                    continue;
                                 } else {
                                     // we haven't written content-length yet!
-                                    encoder = Encoder::length(len);
+                                    if !Server::imply_body_only(msg.req_method, msg.head.subject) {
+                                        encoder = Encoder::length(len);
+                                    }
                                     extend(dst, b"content-length: ");
                                     extend(dst, value.as_bytes());
                                     wrote_len = true;
                                     is_name_written = true;
                                     prev_con_len = Some(len);
-                                    continue 'headers;
+                                    continue;
                                 }
                             } else {
                                 warn!("illegal Content-Length value: {:?}", value);
@@ -464,13 +476,13 @@ impl Http1Transaction for Server {
                             }
                         }
                         None => {
-                            // We have no body to actually send,
-                            // but the headers claim a content-length.
-                            // There's only 2 ways this makes sense:
+                            // We have no body to actually send, but the headers claim a
+                            // content-length.  There are 3 ways this makes sense:
                             //
                             // - The header says the length is `0`.
+                            // - The response is NOT_MODIFIED
                             // - This is a response to a `HEAD` request.
-                            if msg.req_method == &Some(Method::HEAD) {
+                            if Server::imply_body_only(msg.req_method, msg.head.subject) {
                                 debug_assert_eq!(encoder, Encoder::length(0));
                             } else {
                                 if value.as_bytes() != b"0" {
@@ -479,7 +491,7 @@ impl Http1Transaction for Server {
                                         value
                                     );
                                 }
-                                continue 'headers;
+                                continue;
                             }
                         }
                     }
@@ -492,9 +504,13 @@ impl Http1Transaction for Server {
                         return Err(crate::Error::new_user_header());
                     }
                     // check that we actually can send a chunked body...
-                    if msg.head.version == Version::HTTP_10
-                        || !Server::can_chunked(msg.req_method, msg.head.subject)
-                    {
+                    if msg.head.version == Version::HTTP_10 {
+                        continue;
+                    } else if !Server::can_have_body(msg.req_method, msg.head.subject) {
+                        warn!(
+                            "illegal transfer-encoding ({:?}) seen for {:?} response to {:?} request",
+                            value, msg.head.subject, msg.req_method
+                        );
                         continue;
                     }
                     wrote_len = true;
@@ -503,7 +519,9 @@ impl Http1Transaction for Server {
                     must_write_chunked = !headers::is_chunked_(&value);
 
                     if !is_name_written {
-                        encoder = Encoder::chunked();
+                        if !Server::imply_body_only(msg.req_method, msg.head.subject) {
+                            encoder = Encoder::chunked();
+                        }
                         is_name_written = true;
                         extend(dst, b"transfer-encoding: ");
                         extend(dst, value.as_bytes());
@@ -511,7 +529,7 @@ impl Http1Transaction for Server {
                         extend(dst, b", ");
                         extend(dst, value.as_bytes());
                     }
-                    continue 'headers;
+                    continue;
                 }
                 header::CONNECTION => {
                     if !is_last && headers::connection_close(&value) {
@@ -525,7 +543,7 @@ impl Http1Transaction for Server {
                         extend(dst, b", ");
                         extend(dst, value.as_bytes());
                     }
-                    continue 'headers;
+                    continue;
                 }
                 header::DATE => {
                     wrote_date = true;
@@ -549,44 +567,50 @@ impl Http1Transaction for Server {
 
         handle_is_name_written!();
 
-        if !wrote_len {
-            encoder = match msg.body {
-                Some(BodyLength::Unknown) => {
-                    if msg.head.version == Version::HTTP_10
-                        || !Server::can_chunked(msg.req_method, msg.head.subject)
-                    {
-                        Encoder::close_delimited()
-                    } else {
-                        extend(dst, b"transfer-encoding: chunked\r\n");
-                        Encoder::chunked()
-                    }
+        // We should provide either a Content-Length or Transfer-Encoding header based on what we
+        // know about the Body, unless:
+        //   - the request was HEAD and the body is Body::empty(), because a service may simply not
+        //     provide generate the real body for a HEAD request.
+        //   - the response is a 304, like a HEAD, may imply things about a particular body, but a
+        //     service probably did not provide the full body in its Response.
+        //   - the response must neither contain nor imply a body.
+        if !wrote_len && Server::can_have_body(msg.req_method, msg.head.subject) {
+            match msg.body {
+                Some(BodyLength::Known(0)) => extend(dst, b"content-length: 0\r\n"),
+                Some(BodyLength::Known(len)) => {
+                    extend(dst, b"content-length: ");
+                    let _ = ::itoa::write(&mut dst, len);
+                    extend(dst, b"\r\n");
                 }
-                None | Some(BodyLength::Known(0)) => {
-                    if msg.head.subject != StatusCode::NOT_MODIFIED {
+                None => {
+                    if !Server::imply_body_only(msg.req_method, msg.head.subject) {
                         extend(dst, b"content-length: 0\r\n");
                     }
-                    Encoder::length(0)
                 }
-                Some(BodyLength::Known(len)) => {
-                    if msg.head.subject == StatusCode::NOT_MODIFIED {
-                        Encoder::length(0)
-                    } else {
-                        extend(dst, b"content-length: ");
-                        let _ = ::itoa::write(&mut dst, len);
-                        extend(dst, b"\r\n");
-                        Encoder::length(len)
+                Some(BodyLength::Unknown) => {
+                    if msg.head.version != Version::HTTP_10 {
+                        extend(dst, b"transfer-encoding: chunked\r\n");
                     }
                 }
             };
+
+            if !Server::imply_body_only(msg.req_method, msg.head.subject) {
+                encoder = match msg.body {
+                    Some(BodyLength::Unknown) if msg.head.version == Version::HTTP_10 => {
+                        Encoder::close_delimited()
+                    }
+                    Some(BodyLength::Unknown) => Encoder::chunked(),
+                    Some(BodyLength::Known(len)) => Encoder::length(len),
+                    None => Encoder::length(0),
+                };
+            }
         }
 
-        if !Server::can_have_body(msg.req_method, msg.head.subject) {
-            trace!(
-                "server body forced to 0; method={:?}, status={:?}",
-                msg.req_method,
-                msg.head.subject
-            );
-            encoder = Encoder::length(0);
+        #[cfg(debug_assertions)]
+        if Server::imply_body_only(msg.req_method, msg.head.subject)
+            || !Server::can_have_body(msg.req_method, msg.head.subject)
+        {
+            assert_eq!(encoder, Encoder::length(0));
         }
 
         // cached date is much faster than formatting every request
@@ -631,24 +655,19 @@ impl Http1Transaction for Server {
 #[cfg(feature = "server")]
 impl Server {
     fn can_have_body(method: &Option<Method>, status: StatusCode) -> bool {
-        Server::can_chunked(method, status)
-    }
-
-    fn can_chunked(method: &Option<Method>, status: StatusCode) -> bool {
-        if method == &Some(Method::HEAD) || method == &Some(Method::CONNECT) && status.is_success()
+        if method == &Some(Method::CONNECT) && status.is_success()
+            || status == StatusCode::NO_CONTENT
+            || status.is_informational()
         {
             false
         } else {
-            match status {
-                // TODO: support for 1xx codes needs improvement everywhere
-                // would be 100...199 => false
-                StatusCode::SWITCHING_PROTOCOLS
-                | StatusCode::NO_CONTENT
-                | StatusCode::NOT_MODIFIED => false,
-                _ => true,
-            }
+            true
         }
     }
+
+    fn imply_body_only(method: &Option<Method>, status: StatusCode) -> bool {
+        method == &Some(Method::HEAD) || status == StatusCode::NOT_MODIFIED
+    }
 }
 
 #[cfg(feature = "client")]
diff --git a/tests/client.rs b/tests/client.rs
index 479500ab3e..f12e316252 100644
--- a/tests/client.rs
+++ b/tests/client.rs
@@ -359,6 +359,25 @@ test! {
         expected: "GET / HTTP/1.1\r\nhost: {addr}\r\n\r\n",
         reply: REPLY_OK,
 
+    client:
+        request: {
+            method: GET,
+            url: "http://{addr}/",
+            // body is Body::empty
+        },
+        response:
+            status: OK,
+            headers: {},
+            body: None,
+}
+
+test! {
+    name: client_get_req_body_explicitly_empty,
+
+    server:
+        expected: "GET / HTTP/1.1\r\nhost: {addr}\r\ncontent-length: 0\r\n\r\n",
+        reply: REPLY_OK,
+
     client:
         request: {
             method: GET,
diff --git a/tests/server.rs b/tests/server.rs
index 72d2e459d8..f0f3fc9901 100644
--- a/tests/server.rs
+++ b/tests/server.rs
@@ -102,13 +102,13 @@ mod response_body_lengths {
 
         let body_str = match case.body {
             Bd::Known(b) => {
-                reply.body(b);
+                reply.body_bytes(b);
                 b
             }
             Bd::Unknown(b) => {
                 let (mut tx, body) = hyper::Body::channel();
                 tx.try_send_data(b.into()).expect("try_send_data");
-                reply.body_stream(body);
+                reply.body(body);
                 b
             }
         };
@@ -322,7 +322,7 @@ mod response_body_lengths {
 
         let server = serve();
         let addr_str = format!("http://{}", server.addr());
-        server.reply().body("Hello, World!");
+        server.reply().body_bytes("Hello, World!");
 
         let client = Client::builder()
             .http2_only(true)
@@ -345,7 +345,7 @@ mod response_body_lengths {
         server
             .reply()
             .header("content-length", "10")
-            .body("Hello, World!");
+            .body_bytes("Hello, World!");
 
         let client = Client::builder()
             .http2_only(true)
@@ -368,7 +368,7 @@ fn get_chunked_response_with_ka() {
     server
         .reply()
         .header("transfer-encoding", "chunked")
-        .body(foo_bar);
+        .body_bytes(foo_bar);
     let mut req = connect(server.addr());
     req.write_all(
         b"\
@@ -388,7 +388,7 @@ fn get_chunked_response_with_ka() {
     server
         .reply()
         .header("content-length", quux.len().to_string())
-        .body(quux);
+        .body_bytes(quux);
     req.write_all(
         b"\
         GET /quux HTTP/1.1\r\n\
@@ -484,7 +484,7 @@ fn head_response_doesnt_send_body() {
     let _ = pretty_env_logger::try_init();
     let foo_bar = b"foo bar baz";
     let server = serve();
-    server.reply().body(foo_bar);
+    server.reply().body_bytes(foo_bar);
     let mut req = connect(server.addr());
     req.write_all(
         b"\
@@ -515,7 +515,7 @@ fn response_does_not_set_chunked_if_body_not_allowed() {
     let server = serve();
     server
         .reply()
-        .status(hyper::StatusCode::NOT_MODIFIED)
+        .status(hyper::StatusCode::NO_CONTENT)
         .header("transfer-encoding", "chunked");
     let mut req = connect(server.addr());
     req.write_all(
@@ -534,7 +534,7 @@ fn response_does_not_set_chunked_if_body_not_allowed() {
     assert!(!response.contains("transfer-encoding"));
 
     let mut lines = response.lines();
-    assert_eq!(lines.next(), Some("HTTP/1.1 304 Not Modified"));
+    assert_eq!(lines.next(), Some("HTTP/1.1 204 No Content"));
 
     // no body or 0\r\n\r\n
     let mut lines = lines.skip_while(|line| !line.is_empty());
@@ -549,7 +549,7 @@ fn keep_alive() {
     server
         .reply()
         .header("content-length", foo_bar.len().to_string())
-        .body(foo_bar);
+        .body_bytes(foo_bar);
     let mut req = connect(server.addr());
     req.write_all(
         b"\
@@ -568,7 +568,7 @@ fn keep_alive() {
     server
         .reply()
         .header("content-length", quux.len().to_string())
-        .body(quux);
+        .body_bytes(quux);
     req.write_all(
         b"\
         GET /quux HTTP/1.1\r\n\
@@ -590,7 +590,7 @@ fn http_10_keep_alive() {
     server
         .reply()
         .header("content-length", foo_bar.len().to_string())
-        .body(foo_bar);
+        .body_bytes(foo_bar);
     let mut req = connect(server.addr());
     req.write_all(
         b"\
@@ -618,7 +618,7 @@ fn http_10_keep_alive() {
     server
         .reply()
         .header("content-length", quux.len().to_string())
-        .body(quux);
+        .body_bytes(quux);
     req.write_all(
         b"\
         GET /quux HTTP/1.0\r\n\
@@ -641,7 +641,7 @@ fn http_10_close_on_no_ka() {
         .reply()
         .version(Version::HTTP_10)
         .header("content-length", foo_bar.len().to_string())
-        .body(foo_bar);
+        .body_bytes(foo_bar);
     let mut req = connect(server.addr());
 
     // The client request with version 1.0 that may have the keep-alive header
@@ -676,7 +676,7 @@ fn disable_keep_alive() {
     server
         .reply()
         .header("content-length", foo_bar.len().to_string())
-        .body(foo_bar);
+        .body_bytes(foo_bar);
     let mut req = connect(server.addr());
     req.write_all(
         b"\
@@ -703,7 +703,7 @@ fn header_connection_close() {
         .reply()
         .header("content-length", foo_bar.len().to_string())
         .header("connection", "close")
-        .body(foo_bar);
+        .body_bytes(foo_bar);
     let mut req = connect(server.addr());
     req.write_all(
         b"\
@@ -844,11 +844,11 @@ fn pipeline_disabled() {
     server
         .reply()
         .header("content-length", "12")
-        .body("Hello World!");
+        .body_bytes("Hello World!");
     server
         .reply()
         .header("content-length", "12")
-        .body("Hello World!");
+        .body_bytes("Hello World!");
 
     req.write_all(
         b"\
@@ -891,11 +891,11 @@ fn pipeline_enabled() {
     server
         .reply()
         .header("content-length", "12")
-        .body("Hello World\n");
+        .body_bytes("Hello World\n");
     server
         .reply()
         .header("content-length", "12")
-        .body("Hello World\n");
+        .body_bytes("Hello World\n");
 
     req.write_all(
         b"\
@@ -1580,7 +1580,7 @@ fn streaming_body() {
     static S: &[&[u8]] = &[&[b'x'; 1_000] as &[u8]; 1_00] as _;
     let b = futures_util::stream::iter(S.iter()).map(|&s| Ok::<_, hyper::Error>(s));
     let b = hyper::Body::wrap_stream(b);
-    server.reply().body_stream(b);
+    server.reply().body(b);
 
     let mut tcp = connect(server.addr());
     tcp.write_all(b"GET / HTTP/1.1\r\n\r\n").unwrap();
@@ -1689,7 +1689,7 @@ fn http2_body_user_error_sends_reset_reason() {
     )));
     let b = hyper::Body::wrap_stream(b);
 
-    server.reply().body_stream(b);
+    server.reply().body(b);
 
     let rt = support::runtime();
 
@@ -1775,12 +1775,12 @@ async fn http2_service_poll_ready_error_sends_goaway() {
 }
 
 #[test]
-fn skips_content_length_for_304_responses() {
+fn keeps_content_length_and_drops_body_for_304_responses_no_cl() {
     let server = serve();
     server
         .reply()
         .status(hyper::StatusCode::NOT_MODIFIED)
-        .body("foo");
+        .body(Body::empty());
     let mut req = connect(server.addr());
     req.write_all(
         b"\
@@ -1795,15 +1795,21 @@ fn skips_content_length_for_304_responses() {
     let mut response = String::new();
     req.read_to_string(&mut response).unwrap();
     assert!(!response.contains("content-length:"));
+    let mut lines = response.lines();
+    assert_eq!(lines.next(), Some("HTTP/1.1 304 Not Modified"));
+
+    let mut lines = lines.skip_while(|line| !line.is_empty());
+    assert_eq!(lines.next(), Some(""));
+    assert_eq!(lines.next(), None);
 }
 
 #[test]
-fn skips_content_length_and_body_for_304_responses() {
+fn keeps_content_length_and_drops_body_for_304_responses() {
     let server = serve();
     server
         .reply()
         .status(hyper::StatusCode::NOT_MODIFIED)
-        .body("foo");
+        .body_bytes("foo");
     let mut req = connect(server.addr());
     req.write_all(
         b"\
@@ -1817,7 +1823,7 @@ fn skips_content_length_and_body_for_304_responses() {
 
     let mut response = String::new();
     req.read_to_string(&mut response).unwrap();
-    assert!(!response.contains("content-length:"));
+    assert!(response.contains("content-length: 3"));
     let mut lines = response.lines();
     assert_eq!(lines.next(), Some("HTTP/1.1 304 Not Modified"));
 
@@ -1826,6 +1832,35 @@ fn skips_content_length_and_body_for_304_responses() {
     assert_eq!(lines.next(), None);
 }
 
+#[test]
+fn skips_content_length_and_body_for_204_response() {
+    let server = serve();
+    server
+        .reply()
+        .status(hyper::StatusCode::NO_CONTENT)
+        .body_bytes("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.contains("content-length:"));
+    let mut lines = response.lines();
+    assert_eq!(lines.next(), Some("HTTP/1.1 204 No Content"));
+
+    let mut lines = lines.skip_while(|line| !line.is_empty());
+    assert_eq!(lines.next(), Some(""));
+    assert_eq!(lines.next(), None);
+}
+
 #[tokio::test]
 async fn http2_keep_alive_detects_unresponsive_client() {
     let _ = pretty_env_logger::try_init();
@@ -2076,15 +2111,11 @@ impl<'a> ReplyBuilder<'a> {
         self
     }
 
-    fn body<T: AsRef<[u8]>>(self, body: T) {
-        self.tx
-            .lock()
-            .unwrap()
-            .send(Reply::Body(body.as_ref().to_vec().into()))
-            .unwrap();
+    fn body_bytes<T: AsRef<[u8]>>(self, body: T) {
+        self.body(body.as_ref().to_vec().into());
     }
 
-    fn body_stream(self, body: Body) {
+    fn body(self, body: Body) {
         self.tx.lock().unwrap().send(Reply::Body(body)).unwrap();
     }