From e4ca20e848ed962d616a2f18d197434755be67e1 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 29 Mar 2026 00:51:06 +0800 Subject: [PATCH] docs: clarify reverse proxy compatibility behavior Document BufferPool usage and explain why trailer fallback and disconnect compatibility logic intentionally mirror the standard library reverse proxy. Add a regression test covering unannounced trailer forwarding so that proxy trailer behavior stays aligned with Go's semantics. --- docs/reverse-proxy.md | 34 ++++++++++++++++++++++++++++++++++ reverseproxy.go | 9 +++++++++ reverseproxy_test.go | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/docs/reverse-proxy.md b/docs/reverse-proxy.md index eb4e47e..626a3b0 100644 --- a/docs/reverse-proxy.md +++ b/docs/reverse-proxy.md @@ -110,6 +110,40 @@ r.ANY("/api/*path", touka.ReverseProxy(touka.ReverseProxyConfig{ 对于 SSE 和无 `Content-Length` 的流式响应,Touka 会自动立即刷新,不依赖该配置。 +### `BufferPool` + +可选。用于为响应体复制过程提供可复用的字节缓冲区,以减少大响应或高并发代理场景下的临时内存分配。 + +如果留空,Touka 会在复制响应体时按需分配默认缓冲区。 + +```go +type bytePool struct { + pool sync.Pool +} + +func (p *bytePool) Get() []byte { + if buf, ok := p.pool.Get().([]byte); ok { + return buf + } + return make([]byte, 32*1024) +} + +func (p *bytePool) Put(buf []byte) { + if cap(buf) >= 32*1024 { + p.pool.Put(buf[:32*1024]) + } +} + +proxyPool := &bytePool{} + +r.ANY("/api/*path", touka.ReverseProxy(touka.ReverseProxyConfig{ + Target: target, + BufferPool: proxyPool, +})) +``` + +通常只有在您已经观察到明显的分配压力,或代理的响应体较大、吞吐较高时,才需要专门配置它。 + ### `ModifyRequest` 在请求真正发往后端前,对出站请求做最后修改。 diff --git a/reverseproxy.go b/reverseproxy.go index bdad3e6..6ae368d 100644 --- a/reverseproxy.go +++ b/reverseproxy.go @@ -359,6 +359,10 @@ func (p *reverseProxyHandler) ServeHTTP(c *Context) { c.Writer.Flush() } + // Keep the stdlib-compatible fallback here. + // If the backend only exposes additional trailer keys after the body has been + // fully read, the trailer map can grow and those values must be written using + // the TrailerPrefix form instead of the pre-announced bare header keys. if len(res.Trailer) == announcedTrailers { reverseProxyCopyHeader(c.Writer.Header(), res.Trailer) return @@ -378,6 +382,11 @@ func (p *reverseProxyHandler) requestContext(c *Context) (context.Context, conte return ctx, func() {} } + // Follow the same compatibility path as net/http/httputil.ReverseProxy: + // request contexts are normally cancelable, but middleware can still replace + // c.Request with one backed by context.Background/TODO or another context with + // a nil Done channel. In that case CloseNotifier still provides disconnect + // propagation for the upstream round trip. rawWriter := reverseProxyBaseResponseWriter(c.Writer) cn, ok := rawWriter.(http.CloseNotifier) if !ok { diff --git a/reverseproxy_test.go b/reverseproxy_test.go index c2e2593..5d9148d 100644 --- a/reverseproxy_test.go +++ b/reverseproxy_test.go @@ -187,6 +187,43 @@ func TestReverseProxyCustomErrorHandler(t *testing.T) { } } +func TestReverseProxyUnannouncedTrailerForwarding(t *testing.T) { + t.Helper() + + backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set(http.TrailerPrefix+"X-Unannounced-Trailer", "later") + w.WriteHeader(http.StatusOK) + _, _ = io.WriteString(w, "streamed") + })) + defer backend.Close() + + target, err := url.Parse(backend.URL) + if err != nil { + t.Fatalf("parse target: %v", err) + } + + engine := New() + engine.GET("/trailers", ReverseProxy(ReverseProxyConfig{Target: target})) + + rr := PerformRequest(engine, http.MethodGet, "/trailers", nil, nil) + resp := rr.Result() + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("read body: %v", err) + } + _ = resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("unexpected status: %d", resp.StatusCode) + } + if string(body) != "streamed" { + t.Fatalf("unexpected body: %q", string(body)) + } + if got := resp.Trailer.Get("X-Unannounced-Trailer"); got != "later" { + t.Fatalf("unexpected unannounced trailer: %q", got) + } +} + func TestReverseProxyProtocolUpgrade(t *testing.T) { t.Helper()