From 01614291fb768f4365083443351923362d84588f Mon Sep 17 00:00:00 2001 From: Sayed Kaif Date: Mon, 6 Jul 2026 15:09:41 +0530 Subject: [PATCH 1/2] match chunked as the final coding in is_chunked_transfer_encoding --- httplib.h | 25 ++++++++++++++++++++----- test/test.cc | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/httplib.h b/httplib.h index ffefe12f2c..73a88ac14f 100644 --- a/httplib.h +++ b/httplib.h @@ -7386,8 +7386,24 @@ inline ReadContentResult read_content_chunked(Stream &strm, T &x, } inline bool is_chunked_transfer_encoding(const Headers &headers) { - return case_ignore::equal( - get_header_value(headers, "Transfer-Encoding", "", 0), "chunked"); + // RFC 9112 6.1: a message is framed with the chunked coding when "chunked" + // is the final transfer coding. The field value may list several codings + // (e.g. "gzip, chunked"), and multiple Transfer-Encoding lines are + // equivalent to a single comma-joined list in received order (RFC 9110 + // 5.3), so match the last coding token of the last field line, + // case-insensitively. + auto rng = headers.equal_range("Transfer-Encoding"); + const std::string *value = nullptr; + for (auto it = rng.first; it != rng.second; ++it) { + value = &it->second; + } + if (!value) { return false; } + + std::string last_coding; + split(value->data(), value->data() + value->size(), ',', + [&](const char *b, const char *e) { last_coding.assign(b, e); }); + + return case_ignore::equal(last_coding, "chunked"); } template @@ -13220,9 +13236,8 @@ ClientImpl::open_stream(const std::string &method, const std::string &path, handle.body_reader_.content_length = content_length; } - auto transfer_encoding = - handle.response->get_header_value("Transfer-Encoding"); - handle.body_reader_.chunked = (transfer_encoding == "chunked"); + handle.body_reader_.chunked = + detail::is_chunked_transfer_encoding(handle.response->headers); auto content_encoding = handle.response->get_header_value("Content-Encoding"); if (!content_encoding.empty()) { diff --git a/test/test.cc b/test/test.cc index 51e0cf0aeb..6b2c2a75bb 100644 --- a/test/test.cc +++ b/test/test.cc @@ -420,6 +420,47 @@ TEST(SanitizeFilenameTest, VariousPatterns) { EXPECT_EQ("", httplib::sanitize_filename(" ")); } +// Forward declaration (see base64_encode note below) so the split build can +// see the symbol from the test. +namespace httplib { +namespace detail { +bool is_chunked_transfer_encoding(const Headers &headers); +} // namespace detail +} // namespace httplib + +TEST(ChunkedTransferEncodingTest, DetectsChunkedAsFinalCoding) { + auto with_te = [](const char *te) { + Headers h; + if (te) { h.emplace("Transfer-Encoding", te); } + return h; + }; + + // Sole coding, matched case-insensitively. + EXPECT_TRUE(detail::is_chunked_transfer_encoding(with_te("chunked"))); + EXPECT_TRUE(detail::is_chunked_transfer_encoding(with_te("Chunked"))); + EXPECT_TRUE(detail::is_chunked_transfer_encoding(with_te("CHUNKED"))); + + // RFC 9112 6.1: chunked as the final coding of a list still frames the body. + EXPECT_TRUE(detail::is_chunked_transfer_encoding(with_te("gzip, chunked"))); + EXPECT_TRUE(detail::is_chunked_transfer_encoding(with_te("gzip,chunked"))); + EXPECT_TRUE(detail::is_chunked_transfer_encoding(with_te("gzip, Chunked "))); + + // Multiple field lines combine in received order; the overall last token + // wins. + { + Headers h; + h.emplace("Transfer-Encoding", "gzip"); + h.emplace("Transfer-Encoding", "chunked"); + EXPECT_TRUE(detail::is_chunked_transfer_encoding(h)); + } + + // Not chunk-framed: chunked absent, or not the final coding. + EXPECT_FALSE(detail::is_chunked_transfer_encoding(with_te("gzip"))); + EXPECT_FALSE(detail::is_chunked_transfer_encoding(with_te("chunked, gzip"))); + EXPECT_FALSE(detail::is_chunked_transfer_encoding(with_te(""))); + EXPECT_FALSE(detail::is_chunked_transfer_encoding(with_te(nullptr))); +} + // Forward declaration: in split builds split.py strips `inline` and moves the // definition into httplib.cc, so detail::base64_encode is not visible from the // public httplib.h. Re-declaring it here lets the tests link against the symbol From f569e2d1c6b9b94d75502f53d8fc2135ce76d6fa Mon Sep 17 00:00:00 2001 From: Sayed Kaif Date: Mon, 13 Jul 2026 01:11:44 +0530 Subject: [PATCH 2/2] is_chunked_transfer_encoding: match final coding on the field value Headers is an unordered_multimap, whose iteration order for repeated keys is unspecified, so walking equal_range to pick the last line was unreliable and failed CI on libstdc++. Read the Transfer-Encoding value and match its last comma-separated coding token instead. Drop the multi-line ordering case from the unit test, since the container cannot represent received order across duplicate field lines. --- httplib.h | 15 ++++----------- test/test.cc | 9 --------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/httplib.h b/httplib.h index 73a88ac14f..60acf2d542 100644 --- a/httplib.h +++ b/httplib.h @@ -7388,19 +7388,12 @@ inline ReadContentResult read_content_chunked(Stream &strm, T &x, inline bool is_chunked_transfer_encoding(const Headers &headers) { // RFC 9112 6.1: a message is framed with the chunked coding when "chunked" // is the final transfer coding. The field value may list several codings - // (e.g. "gzip, chunked"), and multiple Transfer-Encoding lines are - // equivalent to a single comma-joined list in received order (RFC 9110 - // 5.3), so match the last coding token of the last field line, - // case-insensitively. - auto rng = headers.equal_range("Transfer-Encoding"); - const std::string *value = nullptr; - for (auto it = rng.first; it != rng.second; ++it) { - value = &it->second; - } - if (!value) { return false; } + // (e.g. "gzip, chunked"), so match the last coding token case-insensitively + // rather than comparing the whole field value against "chunked". + auto value = get_header_value(headers, "Transfer-Encoding", "", 0); std::string last_coding; - split(value->data(), value->data() + value->size(), ',', + split(value, value + std::strlen(value), ',', [&](const char *b, const char *e) { last_coding.assign(b, e); }); return case_ignore::equal(last_coding, "chunked"); diff --git a/test/test.cc b/test/test.cc index 6b2c2a75bb..df9c9e3ee8 100644 --- a/test/test.cc +++ b/test/test.cc @@ -445,15 +445,6 @@ TEST(ChunkedTransferEncodingTest, DetectsChunkedAsFinalCoding) { EXPECT_TRUE(detail::is_chunked_transfer_encoding(with_te("gzip,chunked"))); EXPECT_TRUE(detail::is_chunked_transfer_encoding(with_te("gzip, Chunked "))); - // Multiple field lines combine in received order; the overall last token - // wins. - { - Headers h; - h.emplace("Transfer-Encoding", "gzip"); - h.emplace("Transfer-Encoding", "chunked"); - EXPECT_TRUE(detail::is_chunked_transfer_encoding(h)); - } - // Not chunk-framed: chunked absent, or not the final coding. EXPECT_FALSE(detail::is_chunked_transfer_encoding(with_te("gzip"))); EXPECT_FALSE(detail::is_chunked_transfer_encoding(with_te("chunked, gzip")));