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, 02:15 UTC
encode: ignore request no-transform, vary identity responses on Accept-Encoding (#8301)
Co-authored-by: elee1766 <a@tuxpa.in>
Kévin Dunglas committed
Oct 31, 2026 at 22:24 UTC
a72377a10dac971dc479d96f1d7b11108fd268b4
2 files changed
+99
-40
modules/caddyhttp/encode/encode.go
+33
-26
@@ -170,32 +170,35 @@ func hasNonEmptyValue(h http.Header, key string) bool {
170
}
171
172
func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
173
- if !httpguts.HeaderValuesContainsToken(r.Header["Cache-Control"], "no-transform") {
174
- for _, encName := range AcceptedEncodings(r, enc.Prefer) {
175
- if _, ok := enc.writerPools[encName]; !ok {
176
- continue // encoding not offered
177
- }
178
- w = enc.openResponseWriter(encName, w, r.Method == http.MethodConnect)
179
- defer w.(*responseWriter).Close()
180
-
181
- // to comply with RFC 9110 section 8.8.3(.3), we modify the Etag when encoding
182
- // by appending a hyphen and the encoder name; the problem is, the client will
183
- // send back that Etag in an If-None-Match header, but upstream handlers that set
184
- // the Etag in the first place don't know that we appended to their Etag! so here
185
- // we have to strip our addition so the upstream handlers can still honor client
186
- // caches without knowing about our changes...
187
- if etags := r.Header["If-None-Match"]; len(etags) > 0 {
188
- etag := strings.Join(etags, ", ")
189
- stripped := strings.ReplaceAll(etag, "-"+encName+`"`, `"`)
190
- if stripped != etag || len(etags) > 1 {
191
- r.Header["If-None-Match"] = []string{stripped}
192
- }
173
+ var encodingName string
174
+ for _, encName := range AcceptedEncodings(r, enc.Prefer) {
175
+ if _, ok := enc.writerPools[encName]; !ok {
176
+ continue // encoding not offered
177
+ }
178
+ encodingName = encName
179
+
180
+ // to comply with RFC 9110 section 8.8.3(.3), we modify the Etag when encoding
181
+ // by appending a hyphen and the encoder name; the problem is, the client will
182
+ // send back that Etag in an If-None-Match header, but upstream handlers that set
183
+ // the Etag in the first place don't know that we appended to their Etag! so here
184
+ // we have to strip our addition so the upstream handlers can still honor client
185
+ // caches without knowing about our changes...
186
+ if etags := r.Header["If-None-Match"]; len(etags) > 0 {
187
+ etag := strings.Join(etags, ", ")
188
+ stripped := strings.ReplaceAll(etag, "-"+encName+`"`, `"`)
189
+ if stripped != etag || len(etags) > 1 {
190
+ r.Header["If-None-Match"] = []string{stripped}
191
}
194
-
195
- break
192
}
193
+
194
+ break
195
}
196
197
+ // wrap even when no encoding was negotiated so eligible identity responses
198
+ // still carry Vary: Accept-Encoding for shared caches
199
+ w = enc.openResponseWriter(encodingName, w, r.Method == http.MethodConnect)
200
+ defer w.(*responseWriter).Close()
201
+
202
err := next.ServeHTTP(w, r)
203
// If there was an error, disable encoding completely
204
// This prevents corruption when handle_errors processes the response
@@ -255,7 +258,7 @@ func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, w
258
// configured by config.
259
type responseWriter struct {
260
http.ResponseWriter
258
- encodingName string
261
+ encodingName string // empty when the client accepts no offered encoding
262
w Encoder
263
config *Encode
264
statusCode int
@@ -534,16 +537,20 @@ func (rw *responseWriter) init() {
537
538
hdr := rw.Header()
539
540
+ // Only the response no-transform binds us; the request one addresses intermediaries.
541
if !hasNonEmptyValue(hdr, "Content-Encoding") &&
542
!httpguts.HeaderValuesContainsToken(hdr["Cache-Control"], "no-transform") &&
543
rw.config.Match(rw) {
544
+ if !hasVaryValue(hdr, "Accept-Encoding") {
545
+ hdr["Vary"] = append(hdr["Vary"], "Accept-Encoding")
546
+ }
547
+ if rw.encodingName == "" {
548
+ return
549
+ }
550
rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder)
551
rw.w.Reset(rw.ResponseWriter)
552
delete(hdr, "Content-Length") // https://github.com/golang/go/issues/14975
553
hdr["Content-Encoding"] = []string{rw.encodingName}
544
- if !hasVaryValue(hdr, "Accept-Encoding") {
545
- hdr["Vary"] = append(hdr["Vary"], "Accept-Encoding")
546
- }
554
delete(hdr, "Accept-Ranges") // we don't know ranges for dynamically-encoded content
555
556
// strong ETags need to be distinct depending on the encoding ("selected representation")
modules/caddyhttp/encode/encode_conformance_test.go
+66
-14
@@ -234,7 +234,7 @@ var encodeScenarios = []encodeScenario{
234
checkResponse: checkStripsEncodedETagSuffix,
235
},
236
{
237
- name: "request cache-control no-transform prevents encoding",
237
+ name: "request cache-control no-transform does not prevent encoding",
238
method: http.MethodGet,
239
minLength: 1,
240
reqHeaders: func(encoderCase) http.Header {
@@ -243,7 +243,7 @@ var encodeScenarios = []encodeScenario{
243
next: func(encoderCase) caddyhttp.Handler {
244
return conformanceLargeBodyHandler(conformanceContentType)
245
},
246
- checkResponse: checkBypassesEncoding,
246
+ checkResponse: checkEncodesConformanceBody,
247
},
248
{
249
name: "response cache-control no-transform prevents encoding",
@@ -259,18 +259,6 @@ var encodeScenarios = []encodeScenario{
259
},
260
checkResponse: checkBypassesEncoding,
261
},
262
- {
263
- name: "request cache-control no-transform on a later line prevents encoding",
264
- method: http.MethodGet,
265
- minLength: 1,
266
- reqHeaders: func(encoderCase) http.Header {
267
- return http.Header{"Cache-Control": {"no-cache", "no-transform"}}
268
- },
269
- next: func(encoderCase) caddyhttp.Handler {
270
- return conformanceLargeBodyHandler(conformanceContentType)
271
- },
272
- checkResponse: checkBypassesEncoding,
273
- },
262
{
263
name: "response cache-control no-transform on a later line prevents encoding",
264
method: http.MethodGet,
@@ -308,6 +296,42 @@ var encodeScenarios = []encodeScenario{
296
},
297
checkResponse: checkBypassesEncoding,
298
},
299
+ {
300
+ name: "missing accept-encoding keeps identity and varies",
301
+ method: http.MethodGet,
302
+ minLength: 1,
303
+ reqHeaders: func(encoderCase) http.Header {
304
+ return http.Header{"Accept-Encoding": nil}
305
+ },
306
+ next: func(encoderCase) caddyhttp.Handler {
307
+ return conformanceLargeBodyHandler(conformanceContentType)
308
+ },
309
+ checkResponse: checkIdentityVaries,
310
+ },
311
+ {
312
+ name: "identity accept-encoding keeps identity and varies",
313
+ method: http.MethodGet,
314
+ minLength: 1,
315
+ reqHeaders: func(encoderCase) http.Header {
316
+ return http.Header{"Accept-Encoding": {"identity"}}
317
+ },
318
+ next: func(encoderCase) caddyhttp.Handler {
319
+ return conformanceLargeBodyHandler(conformanceContentType)
320
+ },
321
+ checkResponse: checkIdentityVaries,
322
+ },
323
+ {
324
+ name: "missing accept-encoding with content type matcher rejection does not vary",
325
+ method: http.MethodGet,
326
+ minLength: 1,
327
+ reqHeaders: func(encoderCase) http.Header {
328
+ return http.Header{"Accept-Encoding": nil}
329
+ },
330
+ next: func(encoderCase) caddyhttp.Handler {
331
+ return conformanceLargeBodyHandler("image/png")
332
+ },
333
+ checkResponse: checkBypassesEncoding,
334
+ },
335
}
336
337
// TestEncodeResponseSemantics verifies HTTP edge cases (304, HEAD, range, WebSocket,
@@ -486,6 +510,16 @@ func conformanceLargeBodyHandler(contentType string) caddyhttp.Handler {
510
})
511
}
512
513
+func checkEncodesConformanceBody(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) {
514
+ t.Helper()
515
+
516
+ encName := encCase.encoding.AcceptEncoding()
517
+ if got := w.Header().Get("Content-Encoding"); got != encName {
518
+ t.Fatalf("Content-Encoding = %q, want %q", got, encName)
519
+ }
520
+ assertDecompresses(t, encCase, w.Body.Bytes(), conformanceLargeBody())
521
+}
522
+
523
func checkBypassesEncoding(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) {
524
t.Helper()
525
@@ -496,6 +530,24 @@ func checkBypassesEncoding(t *testing.T, w *httptest.ResponseRecorder, encCase e
530
if !bytes.Equal(w.Body.Bytes(), want) {
531
t.Fatalf("body len = %d, want len = %d", w.Body.Len(), len(want))
532
}
533
+ if encode.HasVaryValue(w.Header(), "Accept-Encoding") {
534
+ t.Fatalf("Vary = %q, want no Accept-Encoding", w.Header().Values("Vary"))
535
+ }
536
+}
537
+
538
+func checkIdentityVaries(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) {
539
+ t.Helper()
540
+
541
+ want := conformanceLargeBody()
542
+ if got := w.Header().Get("Content-Encoding"); got != "" {
543
+ t.Fatalf("Content-Encoding = %q, want empty", got)
544
+ }
545
+ if !bytes.Equal(w.Body.Bytes(), want) {
546
+ t.Fatalf("body len = %d, want len = %d", w.Body.Len(), len(want))
547
+ }
548
+ if !encode.HasVaryValue(w.Header(), "Accept-Encoding") {
549
+ t.Fatalf("Vary = %q, want Accept-Encoding", w.Header().Values("Vary"))
550
+ }
551
}
552
553
func checkKeepsUpstreamContentEncoding(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) {