fix(middleware): correct body mutation behavior in ServeHTTP

Refactor ServeHTTP to properly handle response body mutations by:
- Using ResponseModifier to capture response before modification
- Reading body content and allowing middleware to modify it
- Writing modified body back if changed during modification
- Ensuring proper order: RequestModifier before, ResponseModifier after next()

Previously, httputils.NewModifyResponseWriter did not correctly handle
body mutations. The new implementation captures the full response,
allows modification via modifyResponse, and properly writes back any
changes to the body.

Add BodyReader() and SetBody() methods to ResponseModifier to support
reading and replacing response body content.
This commit is contained in:
yusing
2025-11-17 16:32:58 +08:00
parent 9285977495
commit cb642d7b32
2 changed files with 57 additions and 7 deletions

View File

@@ -9,9 +9,9 @@ import (
"github.com/bytedance/sonic"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/route/rules"
"github.com/yusing/godoxy/internal/serialization"
gperr "github.com/yusing/goutils/errs"
httputils "github.com/yusing/goutils/http"
"github.com/yusing/goutils/http/reverseproxy"
)
@@ -184,17 +184,42 @@ func (m *Middleware) ModifyResponse(resp *http.Response) error {
}
func (m *Middleware) ServeHTTP(next http.HandlerFunc, w http.ResponseWriter, r *http.Request) {
if exec, ok := m.impl.(ResponseModifier); ok {
w = httputils.NewModifyResponseWriter(w, r, func(resp *http.Response) error {
return exec.modifyResponse(resp)
})
}
if exec, ok := m.impl.(RequestModifier); ok {
if proceed := exec.before(w, r); !proceed {
return
}
}
next(w, r)
if exec, ok := m.impl.(ResponseModifier); ok {
rm := rules.NewResponseModifier(w)
defer rm.FlushRelease()
next(rm, r)
currentBody := rm.BodyReader()
currentResp := &http.Response{
StatusCode: rm.StatusCode(),
Header: rm.Header(),
ContentLength: int64(rm.ContentLength()),
Body: currentBody,
Request: r,
}
if err := exec.modifyResponse(currentResp); err != nil {
log.Err(err).Str("middleware", m.Name()).Str("url", fullURL(r)).Msg("failed to modify response")
}
// override the response status code
rm.WriteHeader(currentResp.StatusCode)
// overriding the response header is not necessary
// modifyResponse is supposed to write to Header directly instead of assigning a new header map)
// override the content length and body if changed
if currentResp.Body != currentBody {
rm.SetBody(currentResp.Body)
}
} else {
next(w, r)
}
}
func (m *Middleware) LogWarn(req *http.Request) *zerolog.Event {

View File

@@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"net"
"net/http"
@@ -110,6 +111,15 @@ func (rm *ResponseModifier) WriteHeader(code int) {
rm.statusCode = code
}
// BodyReader returns a reader for the response body.
// Every call to this function will return a new reader that starts from the beginning of the buffer.
func (rm *ResponseModifier) BodyReader() io.ReadCloser {
if rm.buf == nil {
return io.NopCloser(bytes.NewReader(nil))
}
return io.NopCloser(bytes.NewReader(rm.buf.Bytes()))
}
func (rm *ResponseModifier) ResetBody() {
if rm.buf == nil {
return
@@ -117,6 +127,21 @@ func (rm *ResponseModifier) ResetBody() {
rm.buf.Reset()
}
func (rm *ResponseModifier) SetBody(r io.ReadCloser) error {
if rm.buf == nil {
rm.buf = rm.bufPool.GetBuffer()
} else {
rm.buf.Reset()
}
_, err := io.Copy(rm.buf, r)
if err != nil {
return fmt.Errorf("failed to copy body: %w", err)
}
r.Close()
return nil
}
func (rm *ResponseModifier) ContentLength() int {
if rm.buf == nil {
return 0