diff --git a/httplib.h b/httplib.h index a755ed7e..e565f8be 100644 --- a/httplib.h +++ b/httplib.h @@ -8087,7 +8087,7 @@ bool prepare_content_receiver(T &x, int &status, bool decompress, size_t payload_max_length, bool &exceed_payload_max_length, U callback) { if (decompress) { - std::string encoding = x.get_header_value("Content-Encoding"); + auto encoding = get_combined_header_value(x.headers, "Content-Encoding"); std::unique_ptr decompressor; if (!encoding.empty()) { @@ -14253,7 +14253,8 @@ ClientImpl::open_stream(const std::string &method, const std::string &path, handle.body_reader_.chunked = detail::is_chunked_transfer_encoding(handle.response->headers); - auto content_encoding = handle.response->get_header_value("Content-Encoding"); + auto content_encoding = detail::get_combined_header_value( + handle.response->headers, "Content-Encoding"); if (!content_encoding.empty()) { // Same policy as prepare_content_receiver(): reject a coding we know about // but were not built with, pass an unrecognized one through as-is. diff --git a/test/test.cc b/test/test.cc index c505cb3d..66b9317b 100644 --- a/test/test.cc +++ b/test/test.cc @@ -11054,6 +11054,47 @@ static const char GZIPPED_HELLO_WORLD[] = { // as "fibre" look like Brotli, and turned a multi-coding value like // "gzip, br" into a Brotli-labeled body, so a decompressor was run over data // it was never meant to see. +// RFC 9110 Section 5.3: a Content-Encoding split over several field lines is +// the same message as the comma-joined one, so both have to be read the same +// way. Reading only the first line made "gzip" followed by "gzip" look like a +// single gzip coding, and a body the sender says was encoded twice was handed +// back after one pass, still compressed but presented as decoded. +TEST(ContentEncodingTest, DuplicateFieldLinesAreTheSameAsTheJoinedValue) { + const std::string body = "\xff\xd8\xff\xe0 not really a jpeg"; + + Server svr; + svr.Get("/split", [&](const Request & /*req*/, Response &res) { + res.set_content(body, "image/jpeg"); + res.headers.emplace("Content-Encoding", "gzip"); + res.headers.emplace("Content-Encoding", "gzip"); + }); + svr.Get("/joined", [&](const Request & /*req*/, Response &res) { + res.set_content(body, "image/jpeg"); + res.set_header("Content-Encoding", "gzip, gzip"); + }); + + auto port = svr.bind_to_any_port(HOST); + thread t = thread([&]() { svr.listen_after_bind(); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + ASSERT_FALSE(svr.is_running()); + }); + + svr.wait_until_ready(); + + Client cli(HOST, port); + + // Two codings is not something cpp-httplib decodes, so both representations + // take the pass-through path rather than one of them being gunzipped once. + for (const char *path : {"/split", "/joined"}) { + auto res = cli.Get(path); + ASSERT_TRUE(res) << path << " -> " << to_string(res.error()); + EXPECT_EQ(StatusCode::OK_200, res->status) << path; + EXPECT_EQ(body, res->body) << path; + } +} + TEST(ContentEncodingTest, SubstringOfACodingIsNotTheCoding) { const std::string body = "\xff\xd8\xff\xe0 not really a jpeg";