Pass unrecognized Content-Encoding values through instead of failing (#2518)

Since 8bba34e, prepare_content_receiver() rejected every non-empty
Content-Encoding that create_decompressor() could not handle, conflating an
unrecognized content coding with a known one whose support was not compiled
in. A response labeled `Content-Encoding: UTF-8` -- a header some servers
misuse to advertise a character set -- therefore failed with 415, which the
client surfaced as the generic Error::Read. Before that commit the raw body
came back untouched.

Restore that behavior: reject only a coding cpp-httplib recognizes but was
not built with, and leave an unrecognized one (including "identity") alone.
Match codings case-insensitively while here, as RFC 9110 8.4.1 requires;
otherwise `Content-Encoding: GZIP` would look unrecognized and hand back a
still-compressed body.

open_stream() had the mirror-image problem. It silently passed the payload
through whenever create_decompressor() returned null, so a gzip response on
a build without zlib reached the caller still compressed, and it never
checked is_valid() -- gzip_decompressor::decompress() only asserts, so a
failed inflateInit2() was undefined behavior in release builds. It now
applies the same policy as the buffered path.

Add Error::UnsupportedContentEncoding so callers can tell this apart from a
read failure, and report an unusable decompressor as Error::Compression. The
status read_content() writes was previously discarded into an uninitialized
dummy_status.
This commit is contained in:
yhirose committed 2026-08-01 21:25:22 -04:00
1 parent a691e531c3
commit 1562f0ec4f
3 files changed
+220 -8

No files matched your search

+68 -8
View File
@@ -1514,6 +1514,7 @@ enum class Error {
UnsupportedAddressFamily,
HTTPParsing,
InvalidRangeHeader,
UnsupportedContentEncoding,
// For internal use only
SSLPeerCouldBeClosed_,
@@ -7121,19 +7122,49 @@ inline bool zstd_decompressor::decompress(const char *data, size_t data_length,
}
#endif
inline bool contains_case_ignore(const std::string &s, const char *token) {
auto token_end = token + std::strlen(token);
return std::search(s.begin(), s.end(), token, token_end, [](char a, char b) {
return case_ignore::to_lower(a) == case_ignore::to_lower(b);
}) != s.end();
}
// Content codings are case-insensitive (RFC 9110 8.4.1). Matching them
// case-sensitively would make a response labeled e.g. "GZIP" look like an
// unknown coding, and its payload would be handed back still compressed.
inline bool is_zlib_encoding(const std::string &encoding) {
return case_ignore::equal(encoding, "gzip") ||
case_ignore::equal(encoding, "deflate");
}
inline bool is_brotli_encoding(const std::string &encoding) {
return contains_case_ignore(encoding, "br");
}
inline bool is_zstd_encoding(const std::string &encoding) {
return contains_case_ignore(encoding, "zstd");
}
// Returns true if the content coding is one cpp-httplib is able to decompress
// when the corresponding support is compiled in.
inline bool is_known_content_encoding(const std::string &encoding) {
return is_zlib_encoding(encoding) || is_brotli_encoding(encoding) ||
is_zstd_encoding(encoding);
}
inline std::unique_ptr<decompressor>
create_decompressor(const std::string &encoding) {
std::unique_ptr<decompressor> decompressor;
if (encoding == "gzip" || encoding == "deflate") {
if (is_zlib_encoding(encoding)) {
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
decompressor = detail::make_unique<gzip_decompressor>();
#endif
} else if (encoding.find("br") != std::string::npos) {
} else if (is_brotli_encoding(encoding)) {
#ifdef CPPHTTPLIB_BROTLI_SUPPORT
decompressor = detail::make_unique<brotli_decompressor>();
#endif
} else if (encoding == "zstd" || encoding.find("zstd") != std::string::npos) {
} else if (is_zstd_encoding(encoding)) {
#ifdef CPPHTTPLIB_ZSTD_SUPPORT
decompressor = detail::make_unique<zstd_decompressor>();
#endif
@@ -7470,9 +7501,12 @@ bool prepare_content_receiver(T &x, int &status,
std::unique_ptr<decompressor> decompressor;
if (!encoding.empty()) {
// A coding we know about but were not built with is an error. An
// unrecognized coding (including "identity") is left alone and the
// payload is passed through as-is, since some servers misuse the header,
// e.g. by sending a character set such as "Content-Encoding: UTF-8".
decompressor = detail::create_decompressor(encoding);
if (!decompressor) {
// Unsupported encoding or no support compiled in
if (!decompressor && detail::is_known_content_encoding(encoding)) {
status = StatusCode::UnsupportedMediaType_415;
return false;
}
@@ -9776,6 +9810,7 @@ inline std::string to_string(const Error error) {
case Error::UnsupportedAddressFamily: return "Unsupported address family";
case Error::HTTPParsing: return "HTTP parsing failed";
case Error::InvalidRangeHeader: return "Invalid Range header";
case Error::UnsupportedContentEncoding: return "Unsupported Content-Encoding";
default: break;
}
@@ -13436,7 +13471,20 @@ ClientImpl::open_stream(const std::string &method, const std::string &path,
auto content_encoding = handle.response->get_header_value("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.
handle.decompressor_ = detail::create_decompressor(content_encoding);
if (!handle.decompressor_) {
if (detail::is_known_content_encoding(content_encoding)) {
handle.error = Error::UnsupportedContentEncoding;
handle.response.reset();
return handle;
}
} else if (!handle.decompressor_->is_valid()) {
handle.error = Error::Compression;
handle.response.reset();
return handle;
}
}
return handle;
@@ -14397,14 +14445,26 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
}
if (res.status != StatusCode::NotModified_304) {
int dummy_status;
auto content_status = 0;
auto max_length = (!has_payload_max_length_ && req.content_receiver)
? (std::numeric_limits<size_t>::max)()
: payload_max_length_;
if (!detail::read_content(strm, res, max_length, dummy_status,
if (!detail::read_content(strm, res, max_length, content_status,
std::move(progress), std::move(out),
decompress_)) {
if (error != Error::Canceled) { error = Error::Read; }
if (error != Error::Canceled) {
// Tell the caller apart from a plain read failure when the body could
// not be decoded because of its Content-Encoding.
switch (content_status) {
case StatusCode::UnsupportedMediaType_415:
error = Error::UnsupportedContentEncoding;
break;
case StatusCode::InternalServerError_500:
error = Error::Compression;
break;
default: error = Error::Read; break;
}
}
output_error_log(error, &req);
return false;
}