From 2e3cb0f9855bf642c37f58e5668004d3c1b267aa Mon Sep 17 00:00:00 2001 From: yhirose Date: Sun, 18 Oct 2026 12:41:23 -0400 Subject: [PATCH] Parse chunked trailers when the stream API decompresses StreamHandle::read() parsed the trailers only on the path without a decompressor, so response->trailers stayed empty for a compressed chunked response. Parse them in one place for both paths, and only after the last chunk: after a failed read it waited another read timeout for a trailer line, and parse_trailers_if_needed() called before the end read body bytes as trailers. --- httplib.h | 37 ++++++++----------------------------- test/test.cc | 11 +++++++++++ 2 files changed, 19 insertions(+), 29 deletions(-) diff --git a/httplib.h b/httplib.h index a902b3d8..e08e882e 100644 --- a/httplib.h +++ b/httplib.h @@ -15941,24 +15941,11 @@ ClientImpl::open_stream(const std::string &method, const std::string &path, inline ssize_t ClientImpl::StreamHandle::read(char *buf, size_t len) { if (!is_valid() || !response) { return -1; } - if (decompressor_) { return read_with_decompression(buf, len); } - auto n = detail::read_body_content(stream_, body_reader_, buf, len); - - if (n <= 0 && body_reader_.chunked && !trailers_parsed_ && stream_) { - trailers_parsed_ = true; - if (body_reader_.chunked_decoder) { - if (!body_reader_.chunked_decoder->parse_trailers_into( - response->trailers, response->headers)) { - return n; - } - } else { - detail::ChunkedDecoder dec(*stream_); - if (!dec.parse_trailers_into(response->trailers, response->headers)) { - return n; - } - } - } + auto n = decompressor_ + ? read_with_decompression(buf, len) + : detail::read_body_content(stream_, body_reader_, buf, len); + if (n == 0) { parse_trailers_if_needed(); } return n; } @@ -16014,22 +16001,14 @@ inline ssize_t ClientImpl::StreamHandle::read_with_decompression(char *buf, } inline void ClientImpl::StreamHandle::parse_trailers_if_needed() { - if (!response || !stream_ || !body_reader_.chunked || trailers_parsed_) { + // The trailers follow the last chunk, so there are none before it + auto &decoder = body_reader_.chunked_decoder; + if (!response || trailers_parsed_ || !decoder || !decoder->finished) { return; } trailers_parsed_ = true; - - const auto bufsiz = 128; - char line_buf[bufsiz]; - detail::stream_line_reader line_reader(*stream_, line_buf, bufsiz); - - if (!line_reader.getline()) { return; } - - if (!detail::parse_trailers(line_reader, response->trailers, - response->headers)) { - return; - } + decoder->parse_trailers_into(response->trailers, response->headers); } namespace detail { diff --git a/test/test.cc b/test/test.cc index 528cb66c..857f8c2a 100644 --- a/test/test.cc +++ b/test/test.cc @@ -23632,6 +23632,17 @@ TEST_F(OpenStreamTest, Gzip) { EXPECT_EQ("gzip", handle.response->get_header_value("Content-Encoding")); EXPECT_EQ(100u * 1024u, read_all(handle).size()); } + +TEST_F(OpenStreamTest, TrailersWithGzip) { + Client cli("127.0.0.1", port_); + auto handle = + cli.open_stream("GET", "/streamed-chunked-with-prohibited-trailer", {}, + {{"Accept-Encoding", "gzip"}}); + ASSERT_TRUE(handle.is_valid()); + EXPECT_EQ("gzip", handle.response->get_header_value("Content-Encoding")); + EXPECT_EQ("123456789", read_all(handle)); + EXPECT_EQ("yes", handle.response->get_trailer_value("X-Allowed")); +} #endif #ifdef CPPHTTPLIB_BROTLI_SUPPORT