@marcel91200 / caddy Mirror · read-only / commits / 04fe1610
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, 03:30 UTC

encode: fix ETag list matching (#8128)

Hitanshu committed Oct 3, 2026 at 19:08 UTC 04fe16102872eba7c017b495a9006fddae4dbdc4
2 files changed +63 -5
modules/caddyhttp/encode/encode.go
+5 -5
@@ -176,11 +176,11 @@ func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
176 // the Etag in the first place don't know that we appended to their Etag! so here
177 // we have to strip our addition so the upstream handlers can still honor client
178 // caches without knowing about our changes...
179 - if etag := r.Header.Get("If-None-Match"); etag != "" && !strings.HasPrefix(etag, "W/") {
180 - ourSuffix := "-" + encName + `"`
181 - if before, ok := strings.CutSuffix(etag, ourSuffix); ok {
182 - etag = before + `"`
183 - r.Header.Set("If-None-Match", etag)
179 + if etags := r.Header.Values("If-None-Match"); len(etags) > 0 {
180 + etag := strings.Join(etags, ", ")
181 + stripped := strings.ReplaceAll(etag, "-"+encName+`"`, `"`)
182 + if stripped != etag || len(etags) > 1 {
183 + r.Header.Set("If-None-Match", stripped)
184 }
185 }
186
modules/caddyhttp/encode/encode_conformance_test.go
+58
@@ -5,7 +5,10 @@ import (
5 "fmt"
6 "net/http"
7 "net/http/httptest"
8 + "slices"
9 + "strings"
10 "testing"
11 + "time"
12
13 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
14 "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
@@ -13,6 +16,61 @@ import (
16
17 const conformanceContentType = "text/plain"
18
19 +func TestEncodeIfNoneMatch(t *testing.T) {
20 + for _, encCase := range standardEncoderCases(t) {
21 + t.Run(encCase.name, func(t *testing.T) {
22 + enc := newEncodeHandler(t, encCase, 1)
23 + for _, tc := range []struct {
24 + name string
25 + values []string
26 + want []string
27 + etag string
28 + status int
29 + }{
30 + {"single", []string{`"response-%s"`}, []string{`"response"`}, `"response"`, 304},
31 + {"matching first", []string{`"response-%s", "stale-%s"`}, []string{`"response", "stale"`}, `"response"`, 304},
32 + {"matching last", []string{`"stale-%s", "response-%s"`}, []string{`"stale", "response"`}, `"response"`, 304},
33 + {"weak", []string{`W/"response-%s"`}, []string{`W/"response"`}, `"response"`, 304},
34 + {"mixed", []string{`W/"stale-%s", "response-%s"`}, []string{`W/"stale", "response"`}, `"response"`, 304},
35 + {"quoted comma", []string{`"response,part-%s", "stale-%s"`}, []string{`"response,part", "stale"`}, `"response,part"`, 304},
36 + {"multiple lines", []string{`"stale-%s"`, `W/"response-%s"`}, []string{`"stale", W/"response"`}, `"response"`, 304},
37 + {"wildcard", []string{`*`}, []string{`*`}, `"response"`, 304},
38 + {"unencoded", []string{`"response"`}, []string{`"response"`}, `"response"`, 304},
39 + {"other suffix", []string{`"response-other"`}, []string{`"response-other"`}, `"response"`, 200},
40 + {"suffix inside tag", []string{`"response-%s-extra"`}, []string{`"response-%s-extra"`}, `"response"`, 200},
41 + {"unterminated", []string{`"response-%s`}, []string{`"response-%s`}, `"response"`, 200},
42 + } {
43 + t.Run(tc.name, func(t *testing.T) {
44 + r := httptest.NewRequest(http.MethodGet, "/", nil)
45 + r.Header.Set("Accept-Encoding", encCase.encoding.AcceptEncoding())
46 + values, want := slices.Clone(tc.values), slices.Clone(tc.want)
47 + for _, list := range [][]string{values, want} {
48 + for i := range list {
49 + list[i] = strings.ReplaceAll(list[i], "%s", encCase.encoding.AcceptEncoding())
50 + }
51 + }
52 + r.Header["If-None-Match"] = values
53 + w := httptest.NewRecorder()
54 + next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
55 + if got := r.Header.Values("If-None-Match"); !slices.Equal(got, want) {
56 + t.Errorf("If-None-Match = %q, want %q", got, want)
57 + }
58 + w.Header().Set("Etag", tc.etag)
59 + http.ServeContent(w, r, "asset.txt", time.Time{}, strings.NewReader("response body"))
60 + return nil
61 + })
62 + if err := enc.ServeHTTP(w, r, next); err != nil {
63 + t.Fatal(err)
64 + }
65 + if w.Code != tc.status {
66 + t.Errorf("status = %d, want %d", w.Code, tc.status)
67 + }
68 + })
69 + }
70 + })
71 + }
72 +}
73 +
74 // TestStandardEncoderContract verifies Reset, Flush, Close, and Reset-after-Close
75 // reuse for each encoder using the same HTML/JSON/JS/CSS payloads as the benchmark suite.
76 func TestStandardEncoderContract(t *testing.T) {