feat(fileserver): add Content-Digest header support (RFC 9530) - #7937
feat(fileserver): add Content-Digest header support (RFC 9530)#7937VedantMadane wants to merge 14 commits into
Conversation
|
Quick note. We need an AI assistance disclosure and CLA to be signed. although CLA assistant can be annoying sometimes, its fixed now :D |
steadytao
left a comment
There was a problem hiding this comment.
Always nice to align with RFCs but we need to do it to-the-letter when doing so. This should also have far more tests; verify exact digest values and add integration coverage for GET, HEAD, 304, ranges, precompressed responses and read/seek failures. In particular, a 206 digest must cover the selected representation data rather than the complete source file.
|
I think this can cause the sendfile optimization to be disabled when used (but only for an HTTP server, no TLS because we don't have kTLS yet).
Should be noted in the docs. |
|
Thank you @steadytao and @francislavoie for the detailed feedback! I have updated the PR to address all your points:
|
|
@steadytao Thank you for the review! I have updated the PR to address your feedback:
|
2ca5667 to
7df7c26
Compare
|
Update (830d664): Content-Digest is emitted only for full 200 responses over the selected on-disk representation (precompressed sidecar when used). A response-writer hook strips the header when ServeContent resolves to 206/304/416 so Range/If-Range/conditionals cannot advertise a digest for different bytes. Integration tests expanded accordingly. |
steadytao
left a comment
There was a problem hiding this comment.
Looking much better. Thank you for your work thus far.
steadytao
left a comment
There was a problem hiding this comment.
Overall the implementation is good, just issues in the way data is processed/handled.
|
Unrelated but we do so still need an AI assistance disclosure as well. Thank you again for the work thus far. |
|
Added AI disclosure to the footer of the body: |
Status vs latest CHANGES_REQUESTED (steadytao)Head Content-Digest over actual response bytes
Dynamic encode
Wrapper order
Tests
No further code changes in this pass; prior inline threads are resolved against this SHA. |
ec640b7 to
0334ae3
Compare
mholt
left a comment
There was a problem hiding this comment.
This is Matt's Codex agent, GPT-5.6 Sol, replying on his behalf.
The bounded buffering and actual-message-byte approach resolves most of the earlier semantic problems, but one correctness hole remains around source read failures. http.ServeContent discards the error returned by its final io.CopyN, so the wrapper must detect a short buffered body before publishing a digest and committing the response. Please add a regression using a failing/short ReadSeeker; this also restores the read-failure coverage requested in the first review. The branch currently conflicts with master, so it will need a refresh after this correction.
…, and sendfile docs
Validate and deduplicate content_digest algorithms in Caddyfile and Provision. Return read/seek/reset errors from digest calculation so failed digests cannot corrupt the response. Expand unit and integration coverage for exact digests, ranges, precompressed responses, and failure paths. Document sendfile impact in Caddyfile docs.
Emit Content-Digest only for status 200 full-body responses by wrapping the ResponseWriter after http.ServeContent chooses the final status. Strip the header for 206/304/416 and other non-200 outcomes so Range, If-Range, and conditionals cannot advertise a digest over different bytes. Hash the open file (precompressed sidecar when selected). Expand tests for GET, HEAD, 304, ranges, If-Range, invalid/multipart ranges, and gzip.
RFC 9530 Content-Digest covers message content. HEAD responses have an empty body, so hashing the on-disk file was incorrect. Emit the empty-content digest (Appendix B.2) for HEAD 200 instead; full-file hashes remain for GET 200. Selected-representation digests belong on Repr-Digest, which this handler does not set.
Buffer ServeContent output so digests cover the written message content (including 206 partial ranges). Strip Content-Digest when encode dynamically re-encodes the body. Add status-override and range regressions.
Buffer response bodies only up to content_digest_max_buffer (default 4MiB). When Content-Length or the written body exceeds the limit, stream without buffering and omit Content-Digest instead of growing an unbounded buffer.
Content-Length on a HEAD response describes the representation payload that would be returned by GET, while the HEAD response body is empty and requires no buffering. Do not omit the empty-content Content-Digest for HEAD when Content-Length exceeds content_digest_max_buffer. Signed-off-by: Vedant Madane <[email protected]>
…ddyfile docs Move Content-Digest algorithms, hash helpers, and response writer to digest.go to keep staticfiles.go organized, and reduce inline Caddyfile doc comments in favor of website documentation. Signed-off-by: Vedant Madane <[email protected]>
…on non-HEAD responses Detect incomplete body reads caused by source read failures before publishing Content-Digest and committing the response. When http.ServeContent discards the error from its final io.CopyN, fail safely rather than advertising a digest for a truncated response. Signed-off-by: Vedant Madane <[email protected]>
0334ae3 to
a922866
Compare
|
Addressed in a922866:
|
steadytao
left a comment
There was a problem hiding this comment.
The previous source-read fix remains incomplete when Content-Length is absent and the aggregate-memory concern is still unresolved. A 4 MiB per-response limit allows concurrent slow clients to retain unbounded aggregate memory. I would simplify the initial feature to responses that can be safely pre-hashed without buffering, omit unsupported range cases or emit the digest as a trailer.
| func (cd *contentDigestResponseWriter) ReadFrom(r io.Reader) (int64, error) { | ||
| if cd.flushed { | ||
| if rf, ok := cd.ResponseWriter.(io.ReaderFrom); ok { | ||
| return rf.ReadFrom(r) | ||
| } | ||
| return io.Copy(cd.ResponseWriter, r) | ||
| } | ||
| return io.Copy(struct{ io.Writer }{cd}, r) | ||
| } |
There was a problem hiding this comment.
http.ServeContent discards the error returned by this ReadFrom call. The Content-Length comparison in finalize() only recovers that error when a length is present. Precompressed range responses can omit Content-Length, allowing a failed read to produce a digest over truncated content. Store the read error on the writer, reject it from finalize() and add a failing precompressed-range regression.
Summary
Fixes #7901
This PR adds optional RFC 9530 Content-Digest header generation to the ile_server handler for static file responses.
Details
I have used help from various AI assistants to create this feature including Grok Build Heavy and Gemini 3.6 Flash Low.