@marcel91200 / caddy Mirror · read-only
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, 01:00 UTC

master
go 645 lines 21.4 KB
Raw
1 package encode_test
2
3 import (
4 "bytes"
5 "fmt"
6 "io"
7 "net/http"
8 "net/http/httptest"
9 "slices"
10 "strings"
11 "testing"
12 "time"
13
14 "golang.org/x/net/http/httpguts"
15
16 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
17 )
18
19 const conformanceContentType = "text/plain"
20
21 func TestEncodeIfNoneMatch(t *testing.T) {
22 for _, encCase := range standardEncoderCases(t) {
23 t.Run(encCase.name, func(t *testing.T) {
24 enc := newEncodeHandler(t, encCase, 1)
25 for _, tc := range []struct {
26 name string
27 values []string
28 want []string
29 etag string
30 status int
31 }{
32 {"single", []string{`"response-%s"`}, []string{`"response"`}, `"response"`, 304},
33 {"matching first", []string{`"response-%s", "stale-%s"`}, []string{`"response", "stale"`}, `"response"`, 304},
34 {"matching last", []string{`"stale-%s", "response-%s"`}, []string{`"stale", "response"`}, `"response"`, 304},
35 {"weak", []string{`W/"response-%s"`}, []string{`W/"response"`}, `"response"`, 304},
36 {"mixed", []string{`W/"stale-%s", "response-%s"`}, []string{`W/"stale", "response"`}, `"response"`, 304},
37 {"quoted comma", []string{`"response,part-%s", "stale-%s"`}, []string{`"response,part", "stale"`}, `"response,part"`, 304},
38 {"multiple lines", []string{`"stale-%s"`, `W/"response-%s"`}, []string{`"stale", W/"response"`}, `"response"`, 304},
39 {"wildcard", []string{`*`}, []string{`*`}, `"response"`, 304},
40 {"unencoded", []string{`"response"`}, []string{`"response"`}, `"response"`, 304},
41 {"other suffix", []string{`"response-other"`}, []string{`"response-other"`}, `"response"`, 200},
42 {"suffix inside tag", []string{`"response-%s-extra"`}, []string{`"response-%s-extra"`}, `"response"`, 200},
43 {"unterminated", []string{`"response-%s`}, []string{`"response-%s`}, `"response"`, 200},
44 } {
45 t.Run(tc.name, func(t *testing.T) {
46 r := httptest.NewRequest(http.MethodGet, "/", nil)
47 r.Header.Set("Accept-Encoding", encCase.encoding.AcceptEncoding())
48 values, want := slices.Clone(tc.values), slices.Clone(tc.want)
49 for _, list := range [][]string{values, want} {
50 for i := range list {
51 list[i] = strings.ReplaceAll(list[i], "%s", encCase.encoding.AcceptEncoding())
52 }
53 }
54 r.Header["If-None-Match"] = values
55 w := httptest.NewRecorder()
56 next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
57 if got := r.Header.Values("If-None-Match"); !slices.Equal(got, want) {
58 t.Errorf("If-None-Match = %q, want %q", got, want)
59 }
60 w.Header().Set("Etag", tc.etag)
61 http.ServeContent(w, r, "asset.txt", time.Time{}, strings.NewReader("response body"))
62 return nil
63 })
64 if err := enc.ServeHTTP(w, r, next); err != nil {
65 t.Fatal(err)
66 }
67 if w.Code != tc.status {
68 t.Errorf("status = %d, want %d", w.Code, tc.status)
69 }
70 })
71 }
72 })
73 }
74 }
75
76 // TestStandardEncoderContract verifies Reset, Flush, Close, and Reset-after-Close
77 // reuse for each encoder using the same HTML/JSON/JS/CSS payloads as the benchmark suite.
78 func TestStandardEncoderContract(t *testing.T) {
79 for _, encCase := range standardEncoderCases(t) {
80 t.Run(encCase.name, func(t *testing.T) {
81 for _, corpus := range benchmarkCorpora(t) {
82 t.Run(corpus.name, func(t *testing.T) {
83 encoder := encCase.encoding.NewEncoder()
84 original := corpus.data
85
86 encodeAndVerifyRoundTrip(t, encCase, encoder, original)
87
88 // Simulate writer-pool reuse: Close → Reset(nil) → Reset(writer).
89 encoder.Reset(nil)
90 encodeAndVerifyRoundTrip(t, encCase, encoder, original)
91 })
92 }
93 })
94 }
95 }
96
97 // TestEncodeCorpusResponse verifies encoded-response semantics (Content-Encoding, Vary,
98 // ETag suffix, header stripping) for each benchmark corpus and encoder.
99 func TestEncodeCorpusResponse(t *testing.T) {
100 for _, encCase := range standardEncoderCases(t) {
101 t.Run(encCase.name, func(t *testing.T) {
102 for _, corpus := range benchmarkCorpora(t) {
103 t.Run(corpus.name, func(t *testing.T) {
104 enc := newEncodeHandler(t, encCase, 1)
105 r := httptest.NewRequest(http.MethodGet, "/", nil)
106 r.Header.Set("Accept-Encoding", encCase.encoding.AcceptEncoding())
107 w := httptest.NewRecorder()
108
109 next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
110 w.Header().Set("Content-Type", corpus.contentType)
111 w.Header().Set("Content-Length", fmt.Sprintf("%d", len(corpus.data)))
112 w.Header().Set("Accept-Ranges", "bytes")
113 w.Header().Set("Etag", `"response"`)
114 _, err := w.Write(corpus.data)
115 return err
116 })
117
118 if err := enc.ServeHTTP(w, r, next); err != nil {
119 t.Fatalf("ServeHTTP() error = %v", err)
120 }
121 checkEncodedCorpusResponse(t, w, encCase, corpus)
122 })
123 }
124 })
125 }
126 }
127
128 type encodeScenario struct {
129 name string
130 method string
131 minLength int
132 reqHeaders func(encoderCase) http.Header
133 checkRequest func(*testing.T, *http.Request)
134 next func(encoderCase) caddyhttp.Handler
135 checkResponse func(*testing.T, *httptest.ResponseRecorder, encoderCase)
136 }
137
138 var encodeScenarios = []encodeScenario{
139 {
140 name: "minimum length prevents encoding",
141 method: http.MethodGet,
142 minLength: 1024,
143 next: func(encoderCase) caddyhttp.Handler {
144 return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
145 w.Header().Set("Content-Type", conformanceContentType)
146 _, err := w.Write([]byte("short"))
147 return err
148 })
149 },
150 checkResponse: checkMinLengthPreventsEncoding,
151 },
152 {
153 name: "not modified adds vary without encoding",
154 method: http.MethodGet,
155 minLength: 1,
156 next: func(encoderCase) caddyhttp.Handler {
157 return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
158 w.WriteHeader(http.StatusNotModified)
159 return nil
160 })
161 },
162 checkResponse: checkNotModifiedVary,
163 },
164 {
165 name: "head response headers can be encoded without body",
166 method: http.MethodHead,
167 minLength: 1,
168 next: func(encoderCase) caddyhttp.Handler {
169 return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
170 w.Header().Set("Content-Type", conformanceContentType)
171 w.Header().Set("Content-Length", "128")
172 return nil
173 })
174 },
175 checkResponse: checkHeadEncodedHeaders,
176 },
177 {
178 name: "range response bypasses encoding",
179 method: http.MethodGet,
180 minLength: 1,
181 reqHeaders: func(encoderCase) http.Header {
182 return http.Header{"Range": {"bytes=0-15"}}
183 },
184 next: func(encoderCase) caddyhttp.Handler {
185 return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
186 w.Header().Set("Content-Type", conformanceContentType)
187 w.Header().Set("Content-Range", "bytes 0-15/128")
188 w.Header().Set("Accept-Ranges", "bytes")
189 w.WriteHeader(http.StatusPartialContent)
190 _, err := w.Write([]byte("0123456789abcdef"))
191 return err
192 })
193 },
194 checkResponse: checkRangeResponseBypassesEncoding,
195 },
196 {
197 name: "websocket handshake bypasses encoding",
198 method: http.MethodGet,
199 minLength: 1,
200 reqHeaders: func(encoderCase) http.Header {
201 return http.Header{
202 "Connection": {"Upgrade"},
203 "Sec-WebSocket-Key": {"dGhlIHNhbXBsZSBub25jZQ=="},
204 "Upgrade": {"websocket"},
205 }
206 },
207 next: func(encoderCase) caddyhttp.Handler {
208 return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
209 w.WriteHeader(http.StatusSwitchingProtocols)
210 return nil
211 })
212 },
213 checkResponse: checkWebSocketBypass,
214 },
215 {
216 name: "strips encoded etag suffix before next handler",
217 method: http.MethodGet,
218 minLength: 1,
219 reqHeaders: func(encCase encoderCase) http.Header {
220 return http.Header{
221 "If-None-Match": {fmt.Sprintf(`"response-%s"`, encCase.encoding.AcceptEncoding())},
222 }
223 },
224 checkRequest: func(t *testing.T, r *http.Request) {
225 if got := r.Header.Get("If-None-Match"); got != `"response"` {
226 t.Fatalf("If-None-Match = %q, want %q", got, `"response"`)
227 }
228 },
229 next: func(encoderCase) caddyhttp.Handler {
230 return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
231 w.WriteHeader(http.StatusNotModified)
232 return nil
233 })
234 },
235 checkResponse: checkStripsEncodedETagSuffix,
236 },
237 {
238 name: "request cache-control no-transform does not prevent encoding",
239 method: http.MethodGet,
240 minLength: 1,
241 reqHeaders: func(encoderCase) http.Header {
242 return http.Header{"Cache-Control": {"no-cache, no-transform"}}
243 },
244 next: func(encoderCase) caddyhttp.Handler {
245 return conformanceLargeBodyHandler(conformanceContentType)
246 },
247 checkResponse: checkEncodesConformanceBody,
248 },
249 {
250 name: "response cache-control no-transform prevents encoding",
251 method: http.MethodGet,
252 minLength: 1,
253 next: func(encoderCase) caddyhttp.Handler {
254 return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
255 w.Header().Set("Content-Type", conformanceContentType)
256 w.Header().Set("Cache-Control", "no-cache, no-transform")
257 _, err := w.Write(conformanceLargeBody())
258 return err
259 })
260 },
261 checkResponse: checkBypassesEncoding,
262 },
263 {
264 name: "response cache-control no-transform on a later line prevents encoding",
265 method: http.MethodGet,
266 minLength: 1,
267 next: func(encoderCase) caddyhttp.Handler {
268 return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
269 w.Header().Set("Content-Type", conformanceContentType)
270 w.Header()["Cache-Control"] = []string{"no-cache", "no-transform"}
271 _, err := w.Write(conformanceLargeBody())
272 return err
273 })
274 },
275 checkResponse: checkBypassesEncoding,
276 },
277 {
278 name: "content-encoding after an empty line prevents encoding",
279 method: http.MethodGet,
280 minLength: 1,
281 next: func(encoderCase) caddyhttp.Handler {
282 return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
283 w.Header().Set("Content-Type", conformanceContentType)
284 w.Header()["Content-Encoding"] = []string{"", "gzip"}
285 _, err := w.Write(conformanceLargeBody())
286 return err
287 })
288 },
289 checkResponse: checkKeepsUpstreamContentEncoding,
290 },
291 {
292 name: "content type matcher rejection prevents encoding",
293 method: http.MethodGet,
294 minLength: 1,
295 next: func(encoderCase) caddyhttp.Handler {
296 return conformanceLargeBodyHandler("image/png")
297 },
298 checkResponse: checkBypassesEncoding,
299 },
300 {
301 name: "missing accept-encoding keeps identity and varies",
302 method: http.MethodGet,
303 minLength: 1,
304 reqHeaders: func(encoderCase) http.Header {
305 return http.Header{"Accept-Encoding": nil}
306 },
307 next: func(encoderCase) caddyhttp.Handler {
308 return conformanceLargeBodyHandler(conformanceContentType)
309 },
310 checkResponse: checkIdentityVaries,
311 },
312 {
313 name: "identity accept-encoding keeps identity and varies",
314 method: http.MethodGet,
315 minLength: 1,
316 reqHeaders: func(encoderCase) http.Header {
317 return http.Header{"Accept-Encoding": {"identity"}}
318 },
319 next: func(encoderCase) caddyhttp.Handler {
320 return conformanceLargeBodyHandler(conformanceContentType)
321 },
322 checkResponse: checkIdentityVaries,
323 },
324 {
325 name: "missing accept-encoding with content type matcher rejection does not vary",
326 method: http.MethodGet,
327 minLength: 1,
328 reqHeaders: func(encoderCase) http.Header {
329 return http.Header{"Accept-Encoding": nil}
330 },
331 next: func(encoderCase) caddyhttp.Handler {
332 return conformanceLargeBodyHandler("image/png")
333 },
334 checkResponse: checkBypassesEncoding,
335 },
336 }
337
338 // TestEncodeResponseSemantics verifies HTTP edge cases (304, HEAD, range, WebSocket,
339 // minimum_length, ETag request rewriting, no-transform, matcher rejection) independent
340 // of the benchmark corpora.
341 func TestEncodeResponseSemantics(t *testing.T) {
342 for _, encCase := range standardEncoderCases(t) {
343 t.Run(encCase.name, func(t *testing.T) {
344 for _, sc := range encodeScenarios {
345 t.Run(sc.name, func(t *testing.T) {
346 runEncodeScenario(t, encCase, sc)
347 })
348 }
349 })
350 }
351 }
352
353 func runEncodeScenario(t *testing.T, encCase encoderCase, sc encodeScenario) {
354 t.Helper()
355
356 enc := newEncodeHandler(t, encCase, sc.minLength)
357 r := httptest.NewRequest(sc.method, "/", nil)
358 r.Header.Set("Accept-Encoding", encCase.encoding.AcceptEncoding())
359 if sc.reqHeaders != nil {
360 for name, values := range sc.reqHeaders(encCase) {
361 r.Header.Del(name)
362 for _, value := range values {
363 r.Header.Add(name, value)
364 }
365 }
366 }
367 w := httptest.NewRecorder()
368
369 var rw http.ResponseWriter = w
370 if sc.method == http.MethodHead {
371 // httptest.ResponseRecorder still stores body writes on HEAD; discard them
372 // so Close() path matches real clients that must not receive a body.
373 rw = noBodyResponseWriter{ResponseRecorder: w}
374 }
375
376 next := sc.next(encCase)
377 if sc.checkRequest != nil {
378 inner := next
379 next = caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
380 sc.checkRequest(t, r)
381 return inner.ServeHTTP(w, r)
382 })
383 }
384
385 if err := enc.ServeHTTP(rw, r, next); err != nil {
386 t.Fatalf("%s: ServeHTTP() error = %v", sc.name, err)
387 }
388 sc.checkResponse(t, w, encCase)
389 }
390
391 // noBodyResponseWriter discards Write data while still allowing the encode
392 // middleware to observe writes for Content-Length / min-length decisions.
393 type noBodyResponseWriter struct {
394 *httptest.ResponseRecorder
395 }
396
397 func (w noBodyResponseWriter) Write(p []byte) (int, error) {
398 return len(p), nil
399 }
400
401 func checkEncodedCorpusResponse(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase, corpus benchmarkCorpus) {
402 t.Helper()
403
404 encName := encCase.encoding.AcceptEncoding()
405 if got := w.Header().Get("Content-Encoding"); got != encName {
406 t.Fatalf("Content-Encoding = %q, want %q", got, encName)
407 }
408 if !httpguts.HeaderValuesContainsToken(w.Header()["Vary"], "Accept-Encoding") {
409 t.Fatalf("Vary = %q, want Accept-Encoding", w.Header().Values("Vary"))
410 }
411 if got := w.Header().Get("Content-Length"); got != "" {
412 t.Fatalf("Content-Length = %q, want empty", got)
413 }
414 if got := w.Header().Get("Accept-Ranges"); got != "" {
415 t.Fatalf("Accept-Ranges = %q, want empty", got)
416 }
417 wantETag := fmt.Sprintf(`"response-%s"`, encName)
418 if got := w.Header().Get("Etag"); got != wantETag {
419 t.Fatalf("Etag = %q, want %q", got, wantETag)
420 }
421 assertDecompresses(t, encCase, w.Body.Bytes(), corpus.data)
422 }
423
424 func checkMinLengthPreventsEncoding(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) {
425 t.Helper()
426
427 if got := w.Header().Get("Content-Encoding"); got != "" {
428 t.Fatalf("Content-Encoding = %q, want empty", got)
429 }
430 if got := w.Body.String(); got != "short" {
431 t.Fatalf("body = %q, want short", got)
432 }
433 }
434
435 func checkNotModifiedVary(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) {
436 t.Helper()
437
438 if got := w.Code; got != http.StatusNotModified {
439 t.Fatalf("status = %d, want %d", got, http.StatusNotModified)
440 }
441 if got := w.Header().Get("Content-Encoding"); got != "" {
442 t.Fatalf("Content-Encoding = %q, want empty", got)
443 }
444 if !httpguts.HeaderValuesContainsToken(w.Header()["Vary"], "Accept-Encoding") {
445 t.Fatalf("Vary = %q, want Accept-Encoding", w.Header().Values("Vary"))
446 }
447 if got := w.Body.Len(); got != 0 {
448 t.Fatalf("body length = %d, want 0", got)
449 }
450 }
451
452 func checkHeadEncodedHeaders(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) {
453 t.Helper()
454
455 if got := w.Header().Get("Content-Encoding"); got != encCase.encoding.AcceptEncoding() {
456 t.Fatalf("Content-Encoding = %q, want %q", got, encCase.encoding.AcceptEncoding())
457 }
458 if got := w.Body.Len(); got != 0 {
459 t.Fatalf("body length = %d, want 0", got)
460 }
461 }
462
463 func checkRangeResponseBypassesEncoding(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) {
464 t.Helper()
465
466 if got := w.Code; got != http.StatusPartialContent {
467 t.Fatalf("status = %d, want %d", got, http.StatusPartialContent)
468 }
469 if got := w.Header().Get("Content-Encoding"); got != "" {
470 t.Fatalf("Content-Encoding = %q, want empty", got)
471 }
472 if got := w.Header().Get("Content-Range"); got != "bytes 0-15/128" {
473 t.Fatalf("Content-Range = %q, want %q", got, "bytes 0-15/128")
474 }
475 if got := w.Header().Get("Accept-Ranges"); got != "bytes" {
476 t.Fatalf("Accept-Ranges = %q, want bytes", got)
477 }
478 if got := w.Body.String(); got != "0123456789abcdef" {
479 t.Fatalf("body = %q, want %q", got, "0123456789abcdef")
480 }
481 }
482
483 func checkWebSocketBypass(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) {
484 t.Helper()
485
486 if got := w.Code; got != http.StatusSwitchingProtocols {
487 t.Fatalf("status = %d, want %d", got, http.StatusSwitchingProtocols)
488 }
489 if got := w.Header().Get("Content-Encoding"); got != "" {
490 t.Fatalf("Content-Encoding = %q, want empty", got)
491 }
492 }
493
494 func checkStripsEncodedETagSuffix(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) {
495 t.Helper()
496
497 if got := w.Code; got != http.StatusNotModified {
498 t.Fatalf("status = %d, want %d", got, http.StatusNotModified)
499 }
500 if !httpguts.HeaderValuesContainsToken(w.Header()["Vary"], "Accept-Encoding") {
501 t.Fatalf("Vary = %q, want Accept-Encoding", w.Header().Values("Vary"))
502 }
503 }
504
505 func conformanceLargeBodyHandler(contentType string) caddyhttp.Handler {
506 body := conformanceLargeBody()
507 return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
508 w.Header().Set("Content-Type", contentType)
509 _, err := w.Write(body)
510 return err
511 })
512 }
513
514 func checkEncodesConformanceBody(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) {
515 t.Helper()
516
517 encName := encCase.encoding.AcceptEncoding()
518 if got := w.Header().Get("Content-Encoding"); got != encName {
519 t.Fatalf("Content-Encoding = %q, want %q", got, encName)
520 }
521 assertDecompresses(t, encCase, w.Body.Bytes(), conformanceLargeBody())
522 }
523
524 func checkBypassesEncoding(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) {
525 t.Helper()
526
527 want := conformanceLargeBody()
528 if got := w.Header().Get("Content-Encoding"); got != "" {
529 t.Fatalf("Content-Encoding = %q, want empty", got)
530 }
531 if !bytes.Equal(w.Body.Bytes(), want) {
532 t.Fatalf("body len = %d, want len = %d", w.Body.Len(), len(want))
533 }
534 if httpguts.HeaderValuesContainsToken(w.Header()["Vary"], "Accept-Encoding") {
535 t.Fatalf("Vary = %q, want no Accept-Encoding", w.Header().Values("Vary"))
536 }
537 }
538
539 func checkIdentityVaries(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) {
540 t.Helper()
541
542 want := conformanceLargeBody()
543 if got := w.Header().Get("Content-Encoding"); got != "" {
544 t.Fatalf("Content-Encoding = %q, want empty", got)
545 }
546 if !bytes.Equal(w.Body.Bytes(), want) {
547 t.Fatalf("body len = %d, want len = %d", w.Body.Len(), len(want))
548 }
549 if !httpguts.HeaderValuesContainsToken(w.Header()["Vary"], "Accept-Encoding") {
550 t.Fatalf("Vary = %q, want Accept-Encoding", w.Header().Values("Vary"))
551 }
552 }
553
554 func checkKeepsUpstreamContentEncoding(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) {
555 t.Helper()
556
557 if got, want := w.Header().Values("Content-Encoding"), []string{"", "gzip"}; !slices.Equal(got, want) {
558 t.Fatalf("Content-Encoding = %q, want %q", got, want)
559 }
560 if want := conformanceLargeBody(); !bytes.Equal(w.Body.Bytes(), want) {
561 t.Fatalf("body len = %d, want len = %d", w.Body.Len(), len(want))
562 }
563 }
564
565 // TestEncodeHeadResponseContentLength verifies that a HEAD response does not
566 // advertise the length of an empty encoded stream.
567 func TestEncodeHeadResponseContentLength(t *testing.T) {
568 for _, encCase := range standardEncoderCases(t) {
569 t.Run(encCase.name, func(t *testing.T) {
570 enc := newEncodeHandler(t, encCase, 1)
571 next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
572 w.Header().Set("Content-Type", conformanceContentType)
573 w.Header().Set("Content-Length", "128")
574 return nil
575 })
576 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
577 if err := enc.ServeHTTP(w, r, next); err != nil {
578 t.Errorf("ServeHTTP() error = %v", err)
579 }
580 }))
581 defer srv.Close()
582
583 req, err := http.NewRequest(http.MethodHead, srv.URL, nil)
584 if err != nil {
585 t.Fatal(err)
586 }
587 req.Header.Set("Accept-Encoding", encCase.encoding.AcceptEncoding())
588 resp, err := srv.Client().Do(req)
589 if err != nil {
590 t.Fatal(err)
591 }
592 resp.Body.Close()
593
594 if got := resp.Header.Get("Content-Encoding"); got != encCase.encoding.AcceptEncoding() {
595 t.Fatalf("Content-Encoding = %q, want %q", got, encCase.encoding.AcceptEncoding())
596 }
597 if resp.ContentLength != -1 {
598 t.Fatalf("ContentLength = %d, want -1", resp.ContentLength)
599 }
600 })
601 }
602 }
603
604 // TestEncodeHeaderOnlyResponseHasNoBody verifies that a response whose header
605 // is written without any body does not get an empty encoded stream as its body.
606 func TestEncodeHeaderOnlyResponseHasNoBody(t *testing.T) {
607 for _, encCase := range standardEncoderCases(t) {
608 t.Run(encCase.name, func(t *testing.T) {
609 enc := newEncodeHandler(t, encCase, 1)
610 next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
611 w.Header().Set("Content-Type", "text/event-stream")
612 w.WriteHeader(http.StatusOK)
613 return nil
614 })
615 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
616 if err := enc.ServeHTTP(w, r, next); err != nil {
617 t.Errorf("ServeHTTP() error = %v", err)
618 }
619 }))
620 defer srv.Close()
621
622 req, err := http.NewRequest(http.MethodGet, srv.URL, nil)
623 if err != nil {
624 t.Fatal(err)
625 }
626 req.Header.Set("Accept-Encoding", encCase.encoding.AcceptEncoding())
627 resp, err := srv.Client().Do(req)
628 if err != nil {
629 t.Fatal(err)
630 }
631 body, err := io.ReadAll(resp.Body)
632 resp.Body.Close()
633 if err != nil {
634 t.Fatal(err)
635 }
636
637 if got := resp.Header.Get("Content-Encoding"); got != encCase.encoding.AcceptEncoding() {
638 t.Fatalf("Content-Encoding = %q, want %q", got, encCase.encoding.AcceptEncoding())
639 }
640 if len(body) != 0 {
641 t.Fatalf("body len = %d, want 0", len(body))
642 }
643 })
644 }
645 }