@marcel91200 / caddy Mirror · read-only / commits / 03e08ee6
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 09:45 UTC

encode: prioritize zstd and br over gzip in content negotiation (#7772)

* fix(encode): prioritize zstd and br over gzip in content negotiation * test(encode): update unit tests to reflect new default priority ties * fix(encode): move default preferences to dynamic encode handler and restore generic negotiation helper * test(encode): call real Provision function in served-response test * test(encode): rename served-response test to TestServeHTTPDefaultEncodingPreference * refactor(encode): use slices.SortStableFunc and httptest.NewRecorder as recommended * refactor(encode): simplify sorting with cmp.Compare and check request error in test * test(encode): fix variable redeclaration in TestServeHTTPDefaultEncodingPreference Fix 'no new variables on left side of :=' error by changing 'err :=' to 'err =' on line 347, since err was already declared on line 332. This fixes the build failure in the encode module tests.

Muhammad Syafri, S.Kom committed May 29, 2026 at 02:26 UTC 03e08ee6a9cfbebed591c765c210c5488b9aefc2
2 files changed +88 -5
modules/caddyhttp/encode/encode.go
+13 -5
@@ -20,12 +20,12 @@
20 package encode
21
22 import (
23 + "cmp"
24 "fmt"
25 "io"
26 "math"
27 "net/http"
28 "slices"
28 - "sort"
29 "strconv"
30 "strings"
31 "sync"
@@ -127,6 +127,14 @@ func (enc *Encode) Provision(ctx caddy.Context) error {
127 }
128 }
129
130 + if len(enc.Prefer) == 0 {
131 + for _, encName := range []string{"zstd", "br", "gzip"} {
132 + if _, ok := enc.writerPools[encName]; ok {
133 + enc.Prefer = append(enc.Prefer, encName)
134 + }
135 + }
136 + }
137 +
138 return nil
139 }
140
@@ -538,11 +546,11 @@ func AcceptedEncodings(r *http.Request, preferredOrder []string) []string {
546 }
547
548 // sort preferences by descending q-factor first, then by preferOrder
541 - sort.Slice(prefs, func(i, j int) bool {
542 - if math.Abs(prefs[i].q-prefs[j].q) < 0.00001 {
543 - return prefs[i].preferOrder > prefs[j].preferOrder
549 + slices.SortStableFunc(prefs, func(a, b encodingPreference) int {
550 + if math.Abs(a.q-b.q) < 0.00001 {
551 + return cmp.Compare(b.preferOrder, a.preferOrder)
552 }
545 - return prefs[i].q > prefs[j].q
553 + return cmp.Compare(b.q, a.q)
554 })
555
556 prefEncNames := make([]string, len(prefs))
modules/caddyhttp/encode/encode_test.go
+75
@@ -1,10 +1,16 @@
1 package encode
2
3 import (
4 + "context"
5 + "io"
6 "net/http"
7 + "net/http/httptest"
8 "slices"
9 "sync"
10 "testing"
11 +
12 + "github.com/caddyserver/caddy/v2"
13 + "github.com/caddyserver/caddy/v2/modules/caddyhttp"
14 )
15
16 func BenchmarkOpenResponseWriter(b *testing.B) {
@@ -295,3 +301,72 @@ func TestIsEncodeAllowed(t *testing.T) {
301 })
302 }
303 }
304 +
305 +type mockEncoder struct{}
306 +
307 +func (mockEncoder) Write(p []byte) (n int, err error) { return len(p), nil }
308 +func (mockEncoder) Close() error { return nil }
309 +func (mockEncoder) Reset(w io.Writer) {}
310 +func (mockEncoder) Flush() error { return nil }
311 +
312 +func TestServeHTTPDefaultEncodingPreference(t *testing.T) {
313 + enc := new(Encode)
314 + enc.MinLength = 1 // compress everything
315 + enc.writerPools = map[string]*sync.Pool{
316 + "gzip": {
317 + New: func() any { return mockEncoder{} },
318 + },
319 + "zstd": {
320 + New: func() any { return mockEncoder{} },
321 + },
322 + }
323 +
324 + // Call Provision() with a valid caddy.Context to exercise the real path
325 + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
326 + defer cancel()
327 + if err := enc.Provision(ctx); err != nil {
328 + t.Fatalf("Provision failed: %v", err)
329 + }
330 +
331 + // Test default preference: zstd preferred over gzip
332 + r, err := http.NewRequest("GET", "/", nil)
333 + if err != nil {
334 + t.Fatalf("error creating request: %v", err)
335 + }
336 + r.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
337 +
338 + w := httptest.NewRecorder()
339 + w.Header().Set("Content-Type", "text/plain")
340 +
341 + next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
342 + w.WriteHeader(http.StatusOK)
343 + _, err := w.Write([]byte("Hello, world! This is a long enough string to satisfy min length if it wasn't 1."))
344 + return err
345 + })
346 +
347 + err = enc.ServeHTTP(w, r, next)
348 + if err != nil {
349 + t.Fatalf("ServeHTTP returned error: %v", err)
350 + }
351 +
352 + // ETag suffix or Content-Encoding header should reflect zstd
353 + contentEncoding := w.Header().Get("Content-Encoding")
354 + if contentEncoding != "zstd" {
355 + t.Errorf("Expected Content-Encoding to be 'zstd' by default, got '%s'", contentEncoding)
356 + }
357 +
358 + // Test explicit user preference: gzip over zstd
359 + enc.Prefer = []string{"gzip", "zstd"}
360 +
361 + w2 := httptest.NewRecorder()
362 + w2.Header().Set("Content-Type", "text/plain")
363 + err = enc.ServeHTTP(w2, r, next)
364 + if err != nil {
365 + t.Fatalf("ServeHTTP returned error: %v", err)
366 + }
367 +
368 + contentEncoding2 := w2.Header().Get("Content-Encoding")
369 + if contentEncoding2 != "gzip" {
370 + t.Errorf("Expected Content-Encoding to be 'gzip' when explicitly preferred, got '%s'", contentEncoding2)
371 + }
372 +}