From: Christopher Faulet Date: Wed, 21 Oct 2026 08:37:22 +0000 (+0000) Subject: BUG/MINOR: cache: recognize null qvalues with decimals in Accept-Encoding X-Git-Url: https://git.kaiwu.me/%7B@url%7D?a=commitdiff_plain;h=e97e1309ce1d7f4582b5c4228331c181962b6f7d;p=haproxy.git BUG/MINOR: cache: recognize null qvalues with decimals in Accept-Encoding With "process-vary on", a client refusing an encoding with a null weight written with decimals, such as "Accept-Encoding: gzip;q=0.0, deflate", may be delivered a cached gzip-encoded response, that it explicitly refused and may not be able to decode. Only the client issuing such a request is affected, it receives an encoding it cannot process. The accept-encoding normalizer builds a bitmap of the accepted encodings, from which the ones with a null weight are removed. A null weight was only detected when the parameter was exactly "q=0", while RFC 9110#12.4.2 defines a qvalue as ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ), so "q=0.", "q=0.0", "q=0.00" and "q=0.000" are null weights as well. They were considered as non-null, the encoding was accepted and a cached response with this encoding matched the request's secondary key. The weight is now considered null when "q=0" is only followed by an optional dot and zeroes. The reg-test reg-tests/cache/vary_accept_encoding.vtc is extended: it stores a gzip response then sends requests refusing gzip with "q=0.0" or "q=0.000". It fails without the fix since the cached gzip response is delivered. This patch may be backported to all supported versions. --- diff --git a/reg-tests/cache/vary_accept_encoding.vtc b/reg-tests/cache/vary_accept_encoding.vtc index 6fe2d6f32..dfb8173a4 100644 --- a/reg-tests/cache/vary_accept_encoding.vtc +++ b/reg-tests/cache/vary_accept_encoding.vtc @@ -86,6 +86,38 @@ server s1 { -hdr "Cache-Control: max-age=5" \ -hdr "Content-Encoding: gzip" \ -bodylen 139 + + # Null qvalues with decimals: "q=0.0" and "q=0.000" are null weights + # per RFC 9110#12.4.2, just like "q=0". + rxreq + expect req.url == "/accept-encoding-qvalue" + txresp -hdr "Content-Encoding: gzip" \ + -hdr "Vary: accept-encoding" \ + -hdr "Cache-Control: max-age=5" \ + -bodylen 145 + + rxreq + expect req.url == "/accept-encoding-qvalue" + expect req.http.accept-encoding == "gzip;q=0.0, deflate" + txresp -hdr "Content-Encoding: deflate" \ + -hdr "Vary: accept-encoding" \ + -hdr "Cache-Control: max-age=5" \ + -bodylen 155 + + rxreq + expect req.url == "/accept-encoding-qvalue" + expect req.http.accept-encoding == "gzip;q=0.000, deflate;q=0.00" + txresp -hdr "Vary: accept-encoding" \ + -hdr "Cache-Control: max-age=5" \ + -bodylen 165 + + rxreq + expect req.url == "/accept-encoding-qvalue" + expect req.http.accept-encoding == "gzip;q=0.001" + txresp -hdr "Content-Encoding: gzip" \ + -hdr "Vary: accept-encoding" \ + -hdr "Cache-Control: max-age=5" \ + -bodylen 145 } -start @@ -332,4 +364,38 @@ client c1 -connect ${h1_fe_sock} { expect resp.http.content-encoding == "gzip" expect resp.bodylen == 139 expect resp.http.X-Cache-Hit == 0 + + # + # Null qvalues with decimals (RFC 9110#12.4.2) + # + txreq -url "/accept-encoding-qvalue" -hdr "Accept-Encoding: gzip" + rxresp + expect resp.status == 200 + expect resp.http.content-encoding == "gzip" + expect resp.bodylen == 145 + expect resp.http.X-Cache-Hit == 0 + + # gzip refused with "q=0.0": the cached gzip response is not delivered + txreq -url "/accept-encoding-qvalue" -hdr "Accept-Encoding: gzip;q=0.0, deflate" + rxresp + expect resp.status == 200 + expect resp.http.content-encoding == "deflate" + expect resp.bodylen == 155 + expect resp.http.X-Cache-Hit == 0 + + # both gzip and deflate refused with null qvalues + txreq -url "/accept-encoding-qvalue" -hdr "Accept-Encoding: gzip;q=0.000, deflate;q=0.00" + rxresp + expect resp.status == 200 + expect resp.http.content-encoding == + expect resp.bodylen == 165 + expect resp.http.X-Cache-Hit == 0 + + # a non-null weight still accepts the encoding + txreq -url "/accept-encoding-qvalue" -hdr "Accept-Encoding: gzip;q=0.001" + rxresp + expect resp.status == 200 + expect resp.http.content-encoding == "gzip" + expect resp.bodylen == 145 + expect resp.http.X-Cache-Hit == 1 } -run diff --git a/src/cache.c b/src/cache.c index 7248fa1f6..779bbce2b 100644 --- a/src/cache.c +++ b/src/cache.c @@ -3410,7 +3410,18 @@ static int parse_encoding_value(struct ist encoding, unsigned int *encoding_valu encoding = http_trim_leading_spht(encoding); - *has_null_weight = isteq(encoding, ist("q=0")); + /* qvalue = ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ) + * (RFC 9110#12.4.2), it is null when it only contains zeroes. + */ + if (istmatch(encoding, ist("q=0"))) { + encoding = istadv(encoding, 3); + if (istlen(encoding) && *istptr(encoding) == '.') { + encoding = istnext(encoding); + while (istlen(encoding) && *istptr(encoding) == '0') + encoding = istnext(encoding); + } + *has_null_weight = !istlen(encoding); + } } } }