Issue Details
With encode enabled, a HEAD request for a file that would be compressed gets Content-Length: 20 for gzip and Content-Length: 13 for zstd, whatever the file is. Those are the sizes of an empty gzip and an empty zstd stream. The matching GET either has no Content-Length (chunked) or the real compressed size.
RFC 9110 section 8.6 says a server MUST NOT send Content-Length in a HEAD response unless it equals the length the GET would have sent. Clients that use HEAD to learn a download size get 20 or 13 bytes.
Version: v2.11.7 (release binary) and master at 8507b63.
Caddyfile:
:9080 {
root * .
encode
file_server
}
$ python3 -c 'print("hello caddy\n" * 200, end="")' > asset.txt
$ curl -sI -H 'Accept-Encoding: gzip' http://localhost:9080/asset.txt
HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Type: text/plain; charset=utf-8
Content-Length: 20
$ curl -s -D - -o /dev/null -H 'Accept-Encoding: gzip' http://localhost:9080/asset.txt
HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Type: text/plain; charset=utf-8
Content-Length: 52
$ curl -sI -H 'Accept-Encoding: zstd' http://localhost:9080/asset.txt
HTTP/1.1 200 OK
Content-Encoding: zstd
Content-Type: text/plain; charset=utf-8
Content-Length: 13
(Some headers trimmed. For a larger file the GET is chunked with no Content-Length, and HEAD still says 20.)
Expected: no Content-Length on the HEAD response, as for a dynamically compressed GET (see #4902).
Cause: on HEAD nothing is written, so responseWriter.Close() in modules/caddyhttp/encode/encode.go runs init() to set the encoding headers and then closes the encoder. Closing writes the empty-stream trailer to the client writer, and net/http turns those bytes into Content-Length for the HEAD response. The existing HEAD case in the conformance tests wraps the recorder in a writer that throws writes away, so it does not see these bytes.
I have a small patch with a test (skip Close() on the encoder when no body was written) and can open a PR if this sounds right.
Assistance Disclosure
AI used
If AI was used, describe the extent to which it was used.
Claude Code (Claude Opus) helped write the reproduction, the patch and its test. I reviewed every line and re-ran the steps above myself.
Issue Details
With
encodeenabled, a HEAD request for a file that would be compressed getsContent-Length: 20for gzip andContent-Length: 13for zstd, whatever the file is. Those are the sizes of an empty gzip and an empty zstd stream. The matching GET either has noContent-Length(chunked) or the real compressed size.RFC 9110 section 8.6 says a server MUST NOT send
Content-Lengthin a HEAD response unless it equals the length the GET would have sent. Clients that use HEAD to learn a download size get 20 or 13 bytes.Version: v2.11.7 (release binary) and master at 8507b63.
Caddyfile:
(Some headers trimmed. For a larger file the GET is chunked with no
Content-Length, and HEAD still says 20.)Expected: no
Content-Lengthon the HEAD response, as for a dynamically compressed GET (see #4902).Cause: on HEAD nothing is written, so
responseWriter.Close()inmodules/caddyhttp/encode/encode.gorunsinit()to set the encoding headers and then closes the encoder. Closing writes the empty-stream trailer to the client writer, and net/http turns those bytes intoContent-Lengthfor the HEAD response. The existing HEAD case in the conformance tests wraps the recorder in a writer that throws writes away, so it does not see these bytes.I have a small patch with a test (skip
Close()on the encoder when no body was written) and can open a PR if this sounds right.Assistance Disclosure
AI used
If AI was used, describe the extent to which it was used.
Claude Code (Claude Opus) helped write the reproduction, the patch and its test. I reviewed every line and re-ran the steps above myself.