fix(middleware): allow HTML rewrite for chunked and unknown-length bodies

Relax response-body gating so HTML and XHTML can be buffered when
Transfer-Encoding is chunked-only, or when Content-Length is missing,
while still rejecting non-identity encodings that are not chunked HTML
and other non-HTML cases.

Update modifyHTML to cap reads for unknown length, splice the original
stream back when the cap is hit, and document the behavior in the
package README. Extend tests for themed middleware and the rewrite gate.
This commit is contained in:
yusing
2026-04-22 12:32:39 +08:00
parent 167d54ae57
commit dd33980d18
4 changed files with 107 additions and 19 deletions

View File

@@ -21,7 +21,6 @@ import (
const (
mimeEventStream = "text/event-stream"
headerContentType = "Content-Type"
maxModifiableBody = 4 * 1024 * 1024 // 4MB
)
@@ -294,21 +293,27 @@ func canBufferAndModifyResponseBody(respHeader http.Header) bool {
if err != nil { // skip if invalid content type
return false
}
if hasNonIdentityEncoding(respHeader.Values("Transfer-Encoding")) {
return false
}
if hasNonIdentityEncoding(respHeader.Values("Content-Encoding")) {
return false
}
contentLengthKnown := false
if contentLengthRaw := respHeader.Get("Content-Length"); contentLengthRaw != "" {
contentLength, err := strconv.ParseInt(contentLengthRaw, 10, 64)
if err != nil || contentLength >= maxModifiableBody {
return false
}
contentLengthKnown = true
}
if !isTextLikeMediaType(contentType) {
return false
}
transferEncoding := respHeader.Values("Transfer-Encoding")
if hasNonIdentityEncoding(transferEncoding) {
return isHTMLLikeMediaType(contentType) && isChunkedTransferEncoding(transferEncoding)
}
if !contentLengthKnown {
return isHTMLLikeMediaType(contentType)
}
return true
}
@@ -325,6 +330,24 @@ func hasNonIdentityEncoding(values []string) bool {
return false
}
func isChunkedTransferEncoding(values []string) bool {
foundChunked := false
for _, value := range values {
for token := range strings.SplitSeq(value, ",") {
token = strings.TrimSpace(token)
switch {
case token == "", strings.EqualFold(token, "identity"):
continue
case strings.EqualFold(token, "chunked"):
foundChunked = true
default:
return false
}
}
}
return foundChunked
}
func isTextLikeMediaType(contentType string) bool {
if contentType == "" {
return false
@@ -351,6 +374,10 @@ func isTextLikeMediaType(contentType string) bool {
return contentType == "application/x-www-form-urlencoded"
}
func isHTMLLikeMediaType(contentType string) bool {
return contentType == "text/html" || contentType == "application/xhtml+xml"
}
func (m *Middleware) LogWarn(req *http.Request) *zerolog.Event {
//nolint:zerologlint
return log.Warn().Str("middleware", m.name).