encode: Improve Etag handling (fix #5849)

We also improve Last-Modified handling in the file server.
Both changes should be more compliant with RFC 9110.
This commit is contained in:
Matthew Holt
2024-04-17 19:12:03 -06:00
parent 3efda6fb3a
commit 3067074d9c
4 changed files with 80 additions and 11 deletions

View File

@ -156,6 +156,18 @@ func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
}
w = enc.openResponseWriter(encName, w)
defer w.(*responseWriter).Close()
// to comply with RFC 9110 section 8.8.3(.3), we modify the Etag when encoding
// by appending a hyphen and the encoder name; the problem is, the client will
// send back that Etag in a If-None-Match header, but upstream handlers that set
// the Etag in the first place don't know that we appended to their Etag! so here
// we have to strip our addition so the upstream handlers can still honor client
// caches without knowing about our changes...
if etag := r.Header.Get("If-None-Match"); etag != "" && !strings.HasPrefix(etag, "W/") {
etag = strings.TrimSuffix(etag, "-"+encName+`"`) + `"`
r.Header.Set("If-None-Match", etag)
}
break
}
}
@ -220,6 +232,14 @@ type responseWriter struct {
func (rw *responseWriter) WriteHeader(status int) {
rw.statusCode = status
// See #5849 and RFC 9110 section 15.4.5 (https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4.5) - 304
// Not Modified must have certain headers set as if it was a 200 response, and according to the issue
// we would miss the Vary header in this case when compression was also enabled; note that we set this
// header in the responseWriter.init() method but that is only called if we are writing a response body
if status == http.StatusNotModified {
rw.Header().Add("Vary", "Accept-Encoding")
}
// write status immediately when status code is informational
// see: https://caddy.community/t/disappear-103-early-hints-response-with-encode-enable-caddy-v2-7-6/23081/5
if 100 <= status && status <= 199 {
@ -334,6 +354,18 @@ func (rw *responseWriter) init() {
rw.Header().Set("Content-Encoding", rw.encodingName)
rw.Header().Add("Vary", "Accept-Encoding")
rw.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-encoded content
// strong ETags need to be distinct depending on the encoding ("selected representation")
// see RFC 9110 section 8.8.3.3:
// https://www.rfc-editor.org/rfc/rfc9110.html#name-example-entity-tags-varying
// I don't know a great way to do this... how about appending? That's a neat trick!
// (We have to strip the value we append from If-None-Match headers before
// sending subsequent requests back upstream, however, since upstream handlers
// don't know about our appending to their Etag since they've already done their work)
if etag := rw.Header().Get("Etag"); etag != "" && !strings.HasPrefix(etag, "W/") {
etag = fmt.Sprintf(`%s-%s"`, strings.TrimSuffix(etag, `"`), rw.encodingName)
rw.Header().Set("Etag", etag)
}
}
}