]> git.kaiwu.me - haproxy.git/commitdiff
BUG/MINOR: cache: recognize null qvalues with decimals in Accept-Encoding
authorChristopher Faulet <cfaulet@haproxy.com>
Wed, 21 Oct 2026 08:37:22 +0000 (08:37 +0000)
committerChristopher Faulet <cfaulet@haproxy.com>
Thu, 12 Nov 2026 17:20:31 +0000 (18:20 +0100)
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.

reg-tests/cache/vary_accept_encoding.vtc
src/cache.c

index 6fe2d6f32f77ee889413e3fd7cfbe02b15d77751..dfb8173a45b73d4b3c24ce0d214fedc8a30f0d9f 100644 (file)
@@ -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 == <undef>
+       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
index 7248fa1f66752dd097d5271988493a53be6fedf5..779bbce2bcd97e8999cdeaa62184c26bdb433636 100644 (file)
@@ -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);
+                               }
                        }
                }
        }