Browse Source

Close the connection after a response body with no length

A content provider without a length or chunked coding sends a body that
only the end of the connection delimits (RFC 9112 6.3), yet the response
offered keep-alive, so a keep-alive client waited for the server's idle
timeout. Send Connection: close and close after it, as Go's net/http
does. A Content-Length set by the handler is still dropped; the
overload that takes a length frames such a body.
yhirose 2 days ago
parent
commit
7bf66db6ca
2 changed files with 46 additions and 2 deletions
  1. 9 2
      httplib.h
  2. 37 0
      test/test.cc

+ 9 - 2
httplib.h

@@ -12091,8 +12091,9 @@ inline bool is_partial_content(const Request &req, const Response &res) {
 }
 
 // Sets the Content-Length or Transfer-Encoding framing the body that
-// Server::write_response_core() writes. Only the library sets them: one from a
-// handler would go out next to it, or describe some other body.
+// Server::write_response_core() writes, or closes the connection after a body
+// that has neither. Only the library sets them: one from a handler would go
+// out next to it, or describe some other body.
 inline void set_framing_headers(const Request &req, Response &res,
                                 const std::string &boundary,
                                 const std::string &content_type) {
@@ -12109,6 +12110,12 @@ inline void set_framing_headers(const Request &req, Response &res,
     if (res.content_length_ == 0) {
       if (res.is_chunked_content_provider_) {
         res.set_header("Transfer-Encoding", "chunked");
+      } else if (req.method != "HEAD") {
+        // RFC 9112 6.3: a body with neither ends where the connection does.
+        res.headers.erase("Keep-Alive");
+        if (!has_header_token(res.headers, "Connection", "close")) {
+          res.set_header("Connection", "close");
+        }
       }
       return;
     }

+ 37 - 0
test/test.cc

@@ -5756,6 +5756,43 @@ TEST(ResponseFramingTest, PostRoutingHandlerChangesAreFramed) {
   EXPECT_FALSE(res->has_header("Transfer-Encoding"));
 }
 
+TEST(ResponseFramingTest, BodyWithoutLengthEndsWithTheConnection) {
+  Server svr;
+
+  svr.Get("/", [](const Request & /*req*/, Response &res) {
+    res.set_header("Content-Length", "3");
+    res.set_content_provider("text/plain", [](size_t, DataSink &sink) {
+      sink.write("abc", 3);
+      sink.done();
+      return true;
+    });
+  });
+
+  thread t = thread([&] { svr.listen(HOST, PORT); });
+  auto se = detail::scope_exit([&] {
+    svr.stop();
+    t.join();
+    ASSERT_FALSE(svr.is_running());
+  });
+
+  svr.wait_until_ready();
+
+  Client cli(HOST, PORT);
+  cli.set_keep_alive(true);
+
+  auto res = cli.Get("/");
+  ASSERT_TRUE(res) << "Error: " << to_string(res.error());
+  EXPECT_EQ("abc", res->body);
+  EXPECT_FALSE(res->has_header("Content-Length"));
+  EXPECT_EQ("close", res->get_header_value("Connection"));
+  EXPECT_FALSE(res->has_header("Keep-Alive"));
+
+  // No body to delimit
+  auto head = cli.Head("/");
+  ASSERT_TRUE(head) << "Error: " << to_string(head.error());
+  EXPECT_FALSE(head->has_header("Connection"));
+}
+
 TEST(RoutingHandlerTest, PreAndPostRoutingHandlers) {
 #ifdef CPPHTTPLIB_SSL_ENABLED
   SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);