@marcel91200 / caddy Mirror · read-only / commits / 093fd4b7
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: Don't write an empty encoded stream for HEAD responses (#8150)

* encode: Don't write an empty encoded stream for HEAD responses When nothing was written (e.g. a HEAD request), Close() still closed the encoder, which wrote the empty stream trailer to the client. The server then set Content-Length to that size (20 for gzip, 13 for zstd) instead of omitting it as for the matching GET response. * encode: Check resp.ContentLength in the HEAD test * encode: Track body writes to decide whether to close the encoder

Jay Hemnani committed Oct 9, 2026 at 13:44 UTC 093fd4b7ec3d299c7c9bcfbdad30827de783d36c
2 files changed +97 -1
modules/caddyhttp/encode/encode.go
+14 -1
@@ -252,6 +252,7 @@ type responseWriter struct {
252 config *Encode
253 statusCode int
254 wroteHeader bool
255 + wroteBody bool
256 incremental bool
257 isConnect bool
258 disabled bool // disable encoding for this response
@@ -419,6 +420,9 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
420 } else {
421 n, err = rw.ResponseWriter.Write(p)
422 }
423 + if n > 0 {
424 + rw.wroteBody = true
425 + }
426 if err != nil || !rw.incremental {
427 return n, err
428 }
@@ -466,9 +470,15 @@ func (rw *responseWriter) ReadFrom(r io.Reader) (int64, error) {
470 // the response will be compressed, no sendfile support
471 if rw.w != nil {
472 nr, err := io.Copy(rw.w, r)
473 + if nr > 0 {
474 + rw.wroteBody = true
475 + }
476 return nr + ns, err
477 }
478 nr, err := rf.ReadFrom(r)
479 + if nr > 0 {
480 + rw.wroteBody = true
481 + }
482 return nr + ns, err
483 }
484
@@ -491,7 +501,10 @@ func (rw *responseWriter) Close() error {
501
502 var err error
503 if rw.w != nil {
494 - err = rw.w.Close()
504 + // don't write an empty encoded stream when there was no body
505 + if rw.wroteBody {
506 + err = rw.w.Close()
507 + }
508 rw.w.Reset(nil)
509 rw.config.writerPools[rw.encodingName].Put(rw.w)
510 rw.w = nil
modules/caddyhttp/encode/encode_conformance_test.go
+83
@@ -3,6 +3,7 @@ package encode_test
3 import (
4 "bytes"
5 "fmt"
6 + "io"
7 "net/http"
8 "net/http/httptest"
9 "slices"
@@ -456,3 +457,85 @@ func checkBypassesEncoding(t *testing.T, w *httptest.ResponseRecorder, encCase e
457 t.Fatalf("body len = %d, want len = %d", w.Body.Len(), len(want))
458 }
459 }
460 +
461 +// TestEncodeHeadResponseContentLength verifies that a HEAD response does not
462 +// advertise the length of an empty encoded stream.
463 +func TestEncodeHeadResponseContentLength(t *testing.T) {
464 + for _, encCase := range standardEncoderCases(t) {
465 + t.Run(encCase.name, func(t *testing.T) {
466 + enc := newEncodeHandler(t, encCase, 1)
467 + next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
468 + w.Header().Set("Content-Type", conformanceContentType)
469 + w.Header().Set("Content-Length", "128")
470 + return nil
471 + })
472 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
473 + if err := enc.ServeHTTP(w, r, next); err != nil {
474 + t.Errorf("ServeHTTP() error = %v", err)
475 + }
476 + }))
477 + defer srv.Close()
478 +
479 + req, err := http.NewRequest(http.MethodHead, srv.URL, nil)
480 + if err != nil {
481 + t.Fatal(err)
482 + }
483 + req.Header.Set("Accept-Encoding", encCase.encoding.AcceptEncoding())
484 + resp, err := srv.Client().Do(req)
485 + if err != nil {
486 + t.Fatal(err)
487 + }
488 + resp.Body.Close()
489 +
490 + if got := resp.Header.Get("Content-Encoding"); got != encCase.encoding.AcceptEncoding() {
491 + t.Fatalf("Content-Encoding = %q, want %q", got, encCase.encoding.AcceptEncoding())
492 + }
493 + if resp.ContentLength != -1 {
494 + t.Fatalf("ContentLength = %d, want -1", resp.ContentLength)
495 + }
496 + })
497 + }
498 +}
499 +
500 +// TestEncodeHeaderOnlyResponseHasNoBody verifies that a response whose header
501 +// is written without any body does not get an empty encoded stream as its body.
502 +func TestEncodeHeaderOnlyResponseHasNoBody(t *testing.T) {
503 + for _, encCase := range standardEncoderCases(t) {
504 + t.Run(encCase.name, func(t *testing.T) {
505 + enc := newEncodeHandler(t, encCase, 1)
506 + next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
507 + w.Header().Set("Content-Type", "text/event-stream")
508 + w.WriteHeader(http.StatusOK)
509 + return nil
510 + })
511 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
512 + if err := enc.ServeHTTP(w, r, next); err != nil {
513 + t.Errorf("ServeHTTP() error = %v", err)
514 + }
515 + }))
516 + defer srv.Close()
517 +
518 + req, err := http.NewRequest(http.MethodGet, srv.URL, nil)
519 + if err != nil {
520 + t.Fatal(err)
521 + }
522 + req.Header.Set("Accept-Encoding", encCase.encoding.AcceptEncoding())
523 + resp, err := srv.Client().Do(req)
524 + if err != nil {
525 + t.Fatal(err)
526 + }
527 + body, err := io.ReadAll(resp.Body)
528 + resp.Body.Close()
529 + if err != nil {
530 + t.Fatal(err)
531 + }
532 +
533 + if got := resp.Header.Get("Content-Encoding"); got != encCase.encoding.AcceptEncoding() {
534 + t.Fatalf("Content-Encoding = %q, want %q", got, encCase.encoding.AcceptEncoding())
535 + }
536 + if len(body) != 0 {
537 + t.Fatalf("body len = %d, want 0", len(body))
538 + }
539 + })
540 + }
541 +}