From 4be4be9891652744a5f8376d09c37feca01c53c3 Mon Sep 17 00:00:00 2001
From: Sean McArthur <sean@seanmonstar.com>
Date: Sun, 14 Apr 2024 08:44:05 -0400
Subject: [PATCH] fix(client): send content-length even with no body

Most request methods define a payload. If hyper detects that no body has
been included, it will now include a `content-length: 0` header
automatically.

It will not do this for methods that don't have defined payloads (GET,
HEAD, and CONNECT).
---
 src/proto/h1/role.rs | 23 +++++++++++++++++------
 tests/client.rs      | 24 ++++++++++++++++++++++++
 2 files changed, 41 insertions(+), 6 deletions(-)

diff --git a/src/proto/h1/role.rs b/src/proto/h1/role.rs
index 958340775a..cad25d1ab9 100644
--- a/src/proto/h1/role.rs
+++ b/src/proto/h1/role.rs
@@ -1288,6 +1288,13 @@ impl Client {
             body
         } else {
             head.headers.remove(header::TRANSFER_ENCODING);
+            // If we know there's body coming, set a content-length.
+            // But only if the method normally has a body.
+            // GET, HEAD, and CONNECT are assumed empty.
+            if !is_method_assumed_empty(&head.subject.0) {
+                head.headers
+                    .insert(header::CONTENT_LENGTH, HeaderValue::from_static("0"));
+            }
             return Encoder::length(0);
         };
 
@@ -1361,12 +1368,11 @@ impl Client {
                     // So instead of sending a "chunked" body with a 0-chunk,
                     // assume no body here. If you *must* send a body,
                     // set the headers explicitly.
-                    match head.subject.0 {
-                        Method::GET | Method::HEAD | Method::CONNECT => Some(Encoder::length(0)),
-                        _ => {
-                            te.insert(HeaderValue::from_static("chunked"));
-                            Some(Encoder::chunked())
-                        }
+                    if is_method_assumed_empty(&head.subject.0) {
+                        Some(Encoder::length(0))
+                    } else {
+                        te.insert(HeaderValue::from_static("chunked"));
+                        Some(Encoder::chunked())
                     }
                 } else {
                     None
@@ -1468,6 +1474,11 @@ impl Client {
     }
 }
 
+#[cfg(feature = "client")]
+fn is_method_assumed_empty(method: &Method) -> bool {
+    matches!(method, &Method::GET | &Method::HEAD | &Method::CONNECT)
+}
+
 #[cfg(feature = "client")]
 fn set_content_length(headers: &mut HeaderMap, len: u64) -> Encoder {
     // At this point, there should not be a valid Content-Length
diff --git a/tests/client.rs b/tests/client.rs
index 43e1f08acb..73d080f3f7 100644
--- a/tests/client.rs
+++ b/tests/client.rs
@@ -888,6 +888,30 @@ test! {
             body: None,
 }
 
+test! {
+    name: client_post_empty_auto_length,
+
+    server:
+        expected: "\
+            POST /empty HTTP/1.1\r\n\
+            host: {addr}\r\n\
+            content-length: 0\r\n\
+            \r\n\
+            ",
+        reply: REPLY_OK,
+
+    client:
+        request: {
+            method: POST,
+            url: "http://{addr}/empty",
+            headers: {},
+        },
+        response:
+            status: OK,
+            headers: {},
+            body: None,
+}
+
 test! {
     name: client_head_ignores_body,