From ef1200f6b97551fd729baa0039b31509166c0177 Mon Sep 17 00:00:00 2001 From: Alexander Yastrebov Date: Fri, 14 Jul 2023 03:00:55 +0200 Subject: [PATCH] proxy: set zero content length for default response Content-Length header prevents http.ResponseWriter from writing chunked response. It turns out that reading empty chunked response is slow(er). In particular this change speeds up TrafficSegment tests that perform 10k requests to routes that send no body: ``` r50: Path("/test") && TrafficSegment(0.0, 0.5) -> status(200) -> ; r30: Path("/test") && TrafficSegment(0.5, 0.8) -> status(201) -> ; r20: Path("/test") && TrafficSegment(0.8, 1.0) -> status(202) -> ; ``` Test runtime before the change: ``` $ go test ./predicates/traffic/ -count=1 -run=TestTrafficSegment ok github.com/zalando/skipper/predicates/traffic 9.357s ``` and after: ``` ok github.com/zalando/skipper/predicates/traffic 4.260s ``` Related #1562 Signed-off-by: Alexander Yastrebov --- proxy/context.go | 15 +++++++-------- proxy/defaultresponse_test.go | 26 ++++++++++++++++++++++++++ proxy/proxy.go | 6 ++++-- 3 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 proxy/defaultresponse_test.go diff --git a/proxy/context.go b/proxy/context.go index 418d76ed64..b839039f36 100644 --- a/proxy/context.go +++ b/proxy/context.go @@ -1,7 +1,6 @@ package proxy import ( - "bytes" stdlibcontext "context" "errors" "io" @@ -62,15 +61,15 @@ type noopFlushedResponseWriter struct { ignoredHeader http.Header } -func defaultBody() io.ReadCloser { - return io.NopCloser(&bytes.Buffer{}) +func noBody() io.ReadCloser { + return http.NoBody } func defaultResponse(r *http.Request) *http.Response { return &http.Response{ StatusCode: http.StatusNotFound, Header: make(http.Header), - Body: defaultBody(), + Body: noBody(), Request: r, } } @@ -89,7 +88,7 @@ func cloneRequestMetadata(r *http.Request) *http.Request { ProtoMinor: r.ProtoMinor, Header: cloneHeader(r.Header), Trailer: cloneHeader(r.Trailer), - Body: defaultBody(), + Body: noBody(), ContentLength: r.ContentLength, TransferEncoding: r.TransferEncoding, Close: r.Close, @@ -109,7 +108,7 @@ func cloneResponseMetadata(r *http.Response) *http.Response { ProtoMinor: r.ProtoMinor, Header: cloneHeader(r.Header), Trailer: cloneHeader(r.Trailer), - Body: defaultBody(), + Body: noBody(), ContentLength: r.ContentLength, TransferEncoding: r.TransferEncoding, Close: r.Close, @@ -180,7 +179,7 @@ func (c *context) ensureDefaultResponse() { } if c.response.Body == nil { - c.response.Body = defaultBody() + c.response.Body = noBody() } } @@ -235,7 +234,7 @@ func (c *context) Serve(r *http.Response) { } if r.Body == nil { - r.Body = defaultBody() + r.Body = noBody() } c.servedWithResponse = true diff --git a/proxy/defaultresponse_test.go b/proxy/defaultresponse_test.go new file mode 100644 index 0000000000..1107b415c8 --- /dev/null +++ b/proxy/defaultresponse_test.go @@ -0,0 +1,26 @@ +package proxy_test + +import ( + "net/http" + "testing" + + "github.com/zalando/skipper/eskip" + "github.com/zalando/skipper/proxy/proxytest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDefaultResponse(t *testing.T) { + p := proxytest.Config{ + Routes: eskip.MustParse(`* -> `), + }.Create() + defer p.Close() + + rsp, body, err := p.Client().GetBody(p.URL) + require.NoError(t, err) + + assert.Equal(t, http.StatusNotFound, rsp.StatusCode) + assert.Len(t, body, 0) + assert.Equal(t, "0", rsp.Header.Get("Content-Length")) +} diff --git a/proxy/proxy.go b/proxy/proxy.go index 5922eb4790..485d1a2600 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -958,8 +958,7 @@ func (p *Proxy) rejectBackend(ctx *context, req *http.Request) (*http.Response, if !p.limiters.Get(limit.Settings).Allow(req.Context(), s) { return &http.Response{ StatusCode: limit.StatusCode, - Header: http.Header{"Content-Length": []string{"0"}}, - Body: io.NopCloser(&bytes.Buffer{}), + Body: noBody(), }, true } } @@ -1211,6 +1210,9 @@ func (p *Proxy) serveResponse(ctx *context) { start := time.Now() p.tracing.logStreamEvent(ctx.proxySpan, StreamHeadersEvent, StartEvent) copyHeader(ctx.responseWriter.Header(), ctx.response.Header) + if ctx.response.Body == noBody() { + ctx.responseWriter.Header()["Content-Length"] = []string{"0"} + } if err := ctx.Request().Context().Err(); err != nil { // deadline exceeded or canceled in stdlib, client closed request