Преглед изворни кода

fix(response): prefer server order to client order for response encoding

Use the order the server prefers instead of the client when determing
what compression to use for the response encoding. Currently we prefer
brotli > gzip > deflate regardless of what order the client specifies
them in. We won't use anything a client doesn't specify in
Accept-Encoding or includes with q=0.
Dave Marquard пре 3 месеци
родитељ
комит
5c80660a15
2 измењених фајлова са 57 додато и 34 уклоњено
  1. 19 24
      internal/http/response/encoding.go
  2. 38 10
      internal/http/response/encoding_test.go

+ 19 - 24
internal/http/response/encoding.go

@@ -20,20 +20,22 @@ func AcceptEncoding(accepted ...string) *acceptEncodingParser {
 	return &acceptEncodingParser{accepted: accepted}
 	return &acceptEncodingParser{accepted: accepted}
 }
 }
 
 
-// Parse parses input string according to [HTTP Semantics]
-// and returns first encoding that can be understood by us.
+// Parse the input string according to [HTTP Semantics] and return our
+// most-preferred encoding that the client accepts. Candidates are matched in
+// the order of the accepted list passed to [AcceptEncoding], so the server's
+// preference wins over the order (and weights) the client advertises.
 //
 //
 // Currently this function ignores set weights other than q=0.
 // Currently this function ignores set weights other than q=0.
 // Encodings with q=0 will not be considered.
 // Encodings with q=0 will not be considered.
 //
 //
-// If string is empty or no encoding was accepted function returns "identity".
-//
-// For "identity;q=0" and "*;q=0" function returns an empty string. In that case,
-// if no other encoding was accepted, 406 Not Acceptable should be returned.
+// Returns "identity" if the input is empty or no accepted encoding matches.
 //
 //
 // [HTTP Semantics]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding.
 // [HTTP Semantics]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding.
 func (p *acceptEncodingParser) Parse(acceptEncoding string) string {
 func (p *acceptEncodingParser) Parse(acceptEncoding string) string {
-	accepted := "identity"
+	// bestIdx tracks the lowest index into p.accepted (i.e. the server's most
+	// preferred encoding) seen so far. Tracking it inline avoids collecting the
+	// client's encodings into a slice, keeping this method allocation free.
+	bestIdx := -1
 
 
 	for enc := range strings.SplitSeq(acceptEncoding, ",") {
 	for enc := range strings.SplitSeq(acceptEncoding, ",") {
 		enc = strings.TrimSpace(enc)
 		enc = strings.TrimSpace(enc)
@@ -44,29 +46,22 @@ func (p *acceptEncodingParser) Parse(acceptEncoding string) string {
 			qstr = strings.TrimPrefix(qstr, "q=")
 			qstr = strings.TrimPrefix(qstr, "q=")
 
 
 			q, err := strconv.ParseFloat(qstr, 64)
 			q, err := strconv.ParseFloat(qstr, 64)
-			if err != nil {
+			if err != nil || q == 0 {
 				continue // Ignore weird float values.
 				continue // Ignore weird float values.
 			}
 			}
-
 			enc = strings.TrimSpace(enc[:qi])
 			enc = strings.TrimSpace(enc[:qi])
-
-			if q == 0 && slices.Contains([]string{"identity", "*"}, enc) {
-				accepted = "" // Explicitly disabled, so can't be used as fallback.
-				continue
-			}
-
-			if q == 0 {
-				continue // Skipping unwanted.
-			}
 		}
 		}
 
 
-		if !slices.Contains(p.accepted, enc) {
-			continue // Skipping unsupported.
+		if i := slices.Index(p.accepted, enc); i > -1 && (bestIdx == -1 || i < bestIdx) {
+			bestIdx = i
+			if bestIdx == 0 {
+				break // Nothing can beat the top server preference.
+			}
 		}
 		}
-
-		accepted = enc
-		break
 	}
 	}
 
 
-	return accepted
+	if bestIdx == -1 {
+		return "identity"
+	}
+	return p.accepted[bestIdx]
 }
 }

+ 38 - 10
internal/http/response/encoding_test.go

@@ -24,15 +24,10 @@ func TestAcceptEncoding(t *testing.T) {
 			acceptEncoding: "",
 			acceptEncoding: "",
 			want:           "identity",
 			want:           "identity",
 		},
 		},
-		{
-			name:           "q=0 and identity",
-			acceptEncoding: "identity;q=0",
-			want:           "",
-		},
 		{
 		{
 			name:           "q=0 and *",
 			name:           "q=0 and *",
 			acceptEncoding: "*;q=0",
 			acceptEncoding: "*;q=0",
-			want:           "",
+			want:           "identity",
 		},
 		},
 		{
 		{
 			name:           "gzip",
 			name:           "gzip",
@@ -42,7 +37,7 @@ func TestAcceptEncoding(t *testing.T) {
 		{
 		{
 			name:           "gzip and br",
 			name:           "gzip and br",
 			acceptEncoding: "gzip,br",
 			acceptEncoding: "gzip,br",
-			want:           "gzip",
+			want:           "br",
 		},
 		},
 		{
 		{
 			name:           "br and gzip",
 			name:           "br and gzip",
@@ -68,12 +63,12 @@ func TestAcceptEncoding(t *testing.T) {
 			// We want br here but weights are not supported.
 			// We want br here but weights are not supported.
 			name:           "multiple encodings and q values",
 			name:           "multiple encodings and q values",
 			acceptEncoding: "gzip;q=0.5,br;q=0.8",
 			acceptEncoding: "gzip;q=0.5,br;q=0.8",
-			want:           "gzip",
+			want:           "br",
 		},
 		},
 		{
 		{
 			name:           "multiple encodings and wildcard",
 			name:           "multiple encodings and wildcard",
 			acceptEncoding: "*;q=0,gzip,br",
 			acceptEncoding: "*;q=0,gzip,br",
-			want:           "gzip",
+			want:           "br",
 		},
 		},
 		{
 		{
 			name:           "multiple encodings and wildcard and q=0",
 			name:           "multiple encodings and wildcard and q=0",
@@ -84,7 +79,7 @@ func TestAcceptEncoding(t *testing.T) {
 			// We want br here but weights are not supported.
 			// We want br here but weights are not supported.
 			name:           "multiple encodings and wildcard and q values",
 			name:           "multiple encodings and wildcard and q values",
 			acceptEncoding: "*;q=0.5,gzip;q=0.8,br",
 			acceptEncoding: "*;q=0.5,gzip;q=0.8,br",
-			want:           "gzip",
+			want:           "br",
 		},
 		},
 		{
 		{
 			name:           "multiple encodings and wildcard and q values and q=0",
 			name:           "multiple encodings and wildcard and q values and q=0",
@@ -106,6 +101,39 @@ func TestAcceptEncoding(t *testing.T) {
 			acceptEncoding: "gzip ; q=0.5, deflate;q=0.8",
 			acceptEncoding: "gzip ; q=0.5, deflate;q=0.8",
 			want:           "gzip",
 			want:           "gzip",
 		},
 		},
+		{
+			name:           "default for chrome / safari as of 2026",
+			acceptEncoding: "gzip, deflate, br, zstd",
+			want:           "br",
+		},
+		{
+			// Server preference order wins over the client's listed order.
+			name:           "server preference over client order",
+			acceptEncoding: "deflate, gzip",
+			want:           "gzip",
+		},
+		{
+			// q=0 excludes a specific supported encoding; next server pick wins.
+			name:           "q=0 excludes specific supported encoding",
+			acceptEncoding: "br;q=0, gzip",
+			want:           "gzip",
+		},
+		{
+			// A bare wildcard is not treated as opting into compression.
+			name:           "bare wildcard",
+			acceptEncoding: "*",
+			want:           "identity",
+		},
+		{
+			name:           "identity only",
+			acceptEncoding: "identity",
+			want:           "identity",
+		},
+		{
+			name:           "whitespace-only and empty segments",
+			acceptEncoding: " , gzip , ",
+			want:           "gzip",
+		},
 	}
 	}
 
 
 	for _, test := range tests {
 	for _, test := range tests {