diff --git a/httplib.h b/httplib.h index ffefe12f2c..60acf2d542 100644 --- a/httplib.h +++ b/httplib.h @@ -7386,8 +7386,17 @@ 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"), 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, value + std::strlen(value), ',', + [&](const char *b, const char *e) { last_coding.assign(b, e); }); + + return case_ignore::equal(last_coding, "chunked"); } template @@ -13220,9 +13229,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..df9c9e3ee8 100644 --- a/test/test.cc +++ b/test/test.cc @@ -420,6 +420,38 @@ 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 "))); + + // 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