Match Connection options as tokens rather than whole field values

RFC 9110 Section 7.6.1 defines Connection as a comma-separated list of
case-insensitive connection options, and Section 5.3 lets that list be split
across several field lines. Comparing the whole field value against a single
option gets both wrong.

A client sending "Connection: keep-alive, close" was answered without a
Connection header and its socket was kept open, so the close it asked for was
never performed and never announced. An HTTP/1.0 client asking for keep-alive
only got it by spelling the option exactly "Keep-Alive"; the lowercase form
everyone actually sends closed the connection instead.

Route the five Connection checks through has_header_token(), which already
walks every field line and compares complete tokens. Expect is left alone:
matching "100-continue" as a token would make an unrecognized expectation
alongside it look acceptable, where Section 10.1.1 asks for 417.
This commit is contained in:
yhirose committed 2026-08-18 20:58:34 -04:00
1 parent 2731b728f5
commit 881842cd72
2 files changed
+149 -8

No files matched your search

+11 -8
View File
@@ -9555,9 +9555,11 @@ inline bool has_framed_body(const Request &req) {
}
inline bool is_connection_persistent(const Request &req) {
auto conn = req.get_header_value("Connection");
if (conn == "close") { return false; }
if (req.version == "HTTP/1.0" && conn != "Keep-Alive") { return false; }
if (has_header_token(req.headers, "Connection", "close")) { return false; }
if (req.version == "HTTP/1.0" &&
!has_header_token(req.headers, "Connection", "keep-alive")) {
return false;
}
return true;
}
@@ -12561,7 +12563,8 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
if (need_apply_ranges) { apply_ranges(req, res, content_type, boundary); }
// Prepare additional headers
if (close_connection || req.get_header_value("Connection") == "close" ||
if (close_connection ||
detail::has_header_token(req.headers, "Connection", "close") ||
400 <= res.status) { // Don't leave connections open after errors
res.set_header("Connection", "close");
} else {
@@ -13466,12 +13469,12 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
return write_response(strm, close_connection, req, res);
}
if (req.get_header_value("Connection") == "close") {
if (detail::has_header_token(req.headers, "Connection", "close")) {
connection_closed = true;
}
if (req.version == "HTTP/1.0" &&
req.get_header_value("Connection") != "Keep-Alive") {
!detail::has_header_token(req.headers, "Connection", "keep-alive")) {
connection_closed = true;
}
@@ -13695,7 +13698,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
// consume the next request (issue #2450). If the response has committed the
// connection to close, there is no next request to protect.
if (!req.body_consumed_ && detail::has_framed_body(req)) {
if (res.get_header_value("Connection") == "close") {
if (detail::has_header_token(res.headers, "Connection", "close")) {
connection_closed = true;
} else {
int dummy_status;
@@ -14479,7 +14482,7 @@ inline bool ClientImpl::handle_request(Stream &strm, Request &req,
if (!ret) { return false; }
if (res.get_header_value("Connection") == "close" ||
if (detail::has_header_token(res.headers, "Connection", "close") ||
(res.version == "HTTP/1.0" && res.reason != "Connection established")) {
// NOTE: this requires a not-entirely-obvious chain of calls to be correct
// for this to be safe.