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) {