Add quality parameter parsing for Accept-Encoding header and enhance encoding type selection logic

This commit is contained in:
yhirose
2026-03-14 20:51:25 -04:00
parent 1c3d35f83c
commit bb7c7ab075
2 changed files with 185 additions and 57 deletions
+102 -57
View File
@@ -6471,33 +6471,114 @@ inline bool can_compress_content_type(const std::string &content_type) {
} }
} }
inline bool parse_quality(const char *b, const char *e, std::string &token,
double &quality) {
quality = 1.0;
token.clear();
// Split on first ';': left = token name, right = parameters
const char *params_b = nullptr;
std::size_t params_len = 0;
divide(
b, static_cast<std::size_t>(e - b), ';',
[&](const char *lb, std::size_t llen, const char *rb, std::size_t rlen) {
auto r = trim(lb, lb + llen, 0, llen);
if (r.first < r.second) { token.assign(lb + r.first, lb + r.second); }
params_b = rb;
params_len = rlen;
});
if (token.empty()) { return false; }
if (params_len == 0) { return true; }
// Scan parameters for q= (stops on first match)
bool invalid = false;
split_find(params_b, params_b + params_len, ';',
(std::numeric_limits<size_t>::max)(),
[&](const char *pb, const char *pe) -> bool {
// Match exactly "q=" or "Q=" (not "query=" etc.)
auto len = static_cast<size_t>(pe - pb);
if (len < 2) { return false; }
if ((pb[0] != 'q' && pb[0] != 'Q') || pb[1] != '=') {
return false;
}
// Trim the value portion
auto r = trim(pb, pe, 2, len);
if (r.first >= r.second) {
invalid = true;
return true;
}
double v = 0.0;
auto res = from_chars(pb + r.first, pb + r.second, v);
if (res.ec != std::errc{} || v < 0.0 || v > 1.0) {
invalid = true;
return true;
}
quality = v;
return true;
});
return !invalid;
}
inline EncodingType encoding_type(const Request &req, const Response &res) { inline EncodingType encoding_type(const Request &req, const Response &res) {
auto ret = if (!can_compress_content_type(res.get_header_value("Content-Type"))) {
detail::can_compress_content_type(res.get_header_value("Content-Type")); return EncodingType::None;
if (!ret) { return EncodingType::None; } }
const auto &s = req.get_header_value("Accept-Encoding"); const auto &s = req.get_header_value("Accept-Encoding");
(void)(s); if (s.empty()) { return EncodingType::None; }
// Single-pass: iterate tokens and track the best supported encoding.
// Server preference breaks ties (br > gzip > zstd).
EncodingType best = EncodingType::None;
double best_q = 0.0; // q=0 means "not acceptable"
// Server preference: Brotli > Gzip > Zstd (lower = more preferred)
auto priority = [](EncodingType t) -> int {
switch (t) {
case EncodingType::Brotli: return 0;
case EncodingType::Gzip: return 1;
case EncodingType::Zstd: return 2;
default: return 3;
}
};
std::string name;
split(s.data(), s.data() + s.size(), ',', [&](const char *b, const char *e) {
double quality = 1.0;
if (!parse_quality(b, e, name, quality)) { return; }
if (quality <= 0.0) { return; }
EncodingType type = EncodingType::None;
#ifdef CPPHTTPLIB_BROTLI_SUPPORT #ifdef CPPHTTPLIB_BROTLI_SUPPORT
// TODO: 'Accept-Encoding' has br, not br;q=0 if (case_ignore::equal(name, "br")) { type = EncodingType::Brotli; }
ret = s.find("br") != std::string::npos;
if (ret) { return EncodingType::Brotli; }
#endif #endif
#ifdef CPPHTTPLIB_ZLIB_SUPPORT #ifdef CPPHTTPLIB_ZLIB_SUPPORT
// TODO: 'Accept-Encoding' has gzip, not gzip;q=0 if (type == EncodingType::None && case_ignore::equal(name, "gzip")) {
ret = s.find("gzip") != std::string::npos; type = EncodingType::Gzip;
if (ret) { return EncodingType::Gzip; } }
#endif #endif
#ifdef CPPHTTPLIB_ZSTD_SUPPORT #ifdef CPPHTTPLIB_ZSTD_SUPPORT
// TODO: 'Accept-Encoding' has zstd, not zstd;q=0 if (type == EncodingType::None && case_ignore::equal(name, "zstd")) {
ret = s.find("zstd") != std::string::npos; type = EncodingType::Zstd;
if (ret) { return EncodingType::Zstd; } }
#endif #endif
return EncodingType::None; if (type == EncodingType::None) { return; }
// Higher q-value wins; for equal q, server preference breaks ties
if (quality > best_q ||
(quality == best_q && priority(type) < priority(best))) {
best_q = quality;
best = type;
}
});
return best;
} }
inline bool nocompressor::compress(const char *data, size_t data_length, inline bool nocompressor::compress(const char *data, size_t data_length,
@@ -7613,7 +7694,7 @@ inline bool parse_accept_header(const std::string &s,
struct AcceptEntry { struct AcceptEntry {
std::string media_type; std::string media_type;
double quality; double quality;
int order; // Original order in header int order;
}; };
std::vector<AcceptEntry> entries; std::vector<AcceptEntry> entries;
@@ -7631,48 +7712,12 @@ inline bool parse_accept_header(const std::string &s,
} }
AcceptEntry accept_entry; AcceptEntry accept_entry;
accept_entry.quality = 1.0; // Default quality
accept_entry.order = order++; accept_entry.order = order++;
// Find q= parameter if (!parse_quality(entry.data(), entry.data() + entry.size(),
auto q_pos = entry.find(";q="); accept_entry.media_type, accept_entry.quality)) {
if (q_pos == std::string::npos) { q_pos = entry.find("; q="); } has_invalid_entry = true;
return;
if (q_pos != std::string::npos) {
// Extract media type (before q parameter)
accept_entry.media_type = trim_copy(entry.substr(0, q_pos));
// Extract quality value
auto q_start = entry.find('=', q_pos) + 1;
auto q_end = entry.find(';', q_start);
if (q_end == std::string::npos) { q_end = entry.length(); }
std::string quality_str =
trim_copy(entry.substr(q_start, q_end - q_start));
if (quality_str.empty()) {
has_invalid_entry = true;
return;
}
{
double v = 0.0;
auto res = detail::from_chars(
quality_str.data(), quality_str.data() + quality_str.size(), v);
if (res.ec == std::errc{}) {
accept_entry.quality = v;
} else {
has_invalid_entry = true;
return;
}
}
// Check if quality is in valid range [0.0, 1.0]
if (accept_entry.quality < 0.0 || accept_entry.quality > 1.0) {
has_invalid_entry = true;
return;
}
} else {
// No quality parameter, use entire entry as media type
accept_entry.media_type = entry;
} }
// Remove additional parameters from media type // Remove additional parameters from media type
+83
View File
@@ -1267,6 +1267,89 @@ TEST(ParseAcceptEncoding3, AcceptEncoding) {
#endif #endif
} }
TEST(ParseAcceptEncoding4, AcceptEncodingQZero) {
// All supported encodings rejected with q=0 should return None
Request req;
req.set_header("Accept-Encoding", "gzip;q=0, br;q=0, zstd;q=0, deflate");
Response res;
res.set_header("Content-Type", "text/plain");
auto ret = detail::encoding_type(req, res);
EXPECT_TRUE(ret == detail::EncodingType::None);
}
TEST(ParseAcceptEncoding5, AcceptEncodingQZeroVariants) {
// q=0.0, q=0.00, q=0.000 should also be treated as rejected
Request req;
req.set_header("Accept-Encoding", "gzip;q=0.000, br;q=0.0, zstd;q=0.00");
Response res;
res.set_header("Content-Type", "text/plain");
auto ret = detail::encoding_type(req, res);
EXPECT_TRUE(ret == detail::EncodingType::None);
}
TEST(ParseAcceptEncoding6, AcceptEncodingXGzipQZero) {
// x-gzip;q=0 should not cause "gzip" to be incorrectly detected
Request req;
req.set_header("Accept-Encoding", "x-gzip;q=0");
Response res;
res.set_header("Content-Type", "text/plain");
auto ret = detail::encoding_type(req, res);
EXPECT_TRUE(ret == detail::EncodingType::None);
}
TEST(ParseAcceptEncoding7, AcceptEncodingCaseInsensitive) {
// RFC 7231: Accept-Encoding values are case-insensitive
Request req;
req.set_header("Accept-Encoding", "GZIP, BR, ZSTD");
Response res;
res.set_header("Content-Type", "text/plain");
auto ret = detail::encoding_type(req, res);
#ifdef CPPHTTPLIB_BROTLI_SUPPORT
EXPECT_TRUE(ret == detail::EncodingType::Brotli);
#elif CPPHTTPLIB_ZLIB_SUPPORT
EXPECT_TRUE(ret == detail::EncodingType::Gzip);
#elif CPPHTTPLIB_ZSTD_SUPPORT
EXPECT_TRUE(ret == detail::EncodingType::Zstd);
#else
EXPECT_TRUE(ret == detail::EncodingType::None);
#endif
}
TEST(ParseAcceptEncoding8, AcceptEncodingQValuePriority) {
// q value should determine priority, not hardcoded order
Request req;
req.set_header("Accept-Encoding", "br;q=0.5, gzip;q=1.0, zstd;q=0.8");
Response res;
res.set_header("Content-Type", "text/plain");
auto ret = detail::encoding_type(req, res);
// gzip has highest q=1.0, so it should be selected even though
// br and zstd are also supported
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
EXPECT_TRUE(ret == detail::EncodingType::Gzip);
#elif CPPHTTPLIB_ZSTD_SUPPORT
EXPECT_TRUE(ret == detail::EncodingType::Zstd);
#elif CPPHTTPLIB_BROTLI_SUPPORT
EXPECT_TRUE(ret == detail::EncodingType::Brotli);
#else
EXPECT_TRUE(ret == detail::EncodingType::None);
#endif
}
TEST(BufferStreamTest, read) { TEST(BufferStreamTest, read) {
detail::BufferStream strm1; detail::BufferStream strm1;
Stream &strm = strm1; Stream &strm = strm1;