From 97c112196b53073e559bea38c215793400318487 Mon Sep 17 00:00:00 2001 From: Adam Sax Date: Mon, 24 Oct 2022 14:56:23 -0400 Subject: [PATCH 1/9] wrapped middleware supports Flush() and Hijack() --- httpbp/middlewares.go | 60 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/httpbp/middlewares.go b/httpbp/middlewares.go index 9e7335326..4852aa0b6 100644 --- a/httpbp/middlewares.go +++ b/httpbp/middlewares.go @@ -394,9 +394,10 @@ func recordStatusCode(counters counterGenerator) Middleware { return func(name string, next HandlerFunc) HandlerFunc { counter := counters.Counter("baseplate.http." + name + ".response") return func(ctx context.Context, w http.ResponseWriter, r *http.Request) (err error) { - wrapped := &statusCodeRecorder{ResponseWriter: w} + rec := &statusCodeRecorder{ResponseWriter: w} + wrapped := allowFlushHijack(w, rec) defer func() { - code := wrapped.getCode(err) + code := rec.getCode(err) counter.With("status", statusCodeFamily(code)).Add(1) }() @@ -453,9 +454,11 @@ func PrometheusServerMetrics(_ string) Middleware { } serverActiveRequests.With(activeRequestLabels).Inc() - wrapped := &responseRecorder{ResponseWriter: w} + rec := &responseRecorder{ResponseWriter: w} + wrapped := allowFlushHijack(w, rec) + defer func() { - code := errorCodeForMetrics(wrapped.responseCode, err) + code := errorCodeForMetrics(rec.responseCode, err) success := isRequestSuccessful(code, err) labels := prometheus.Labels{ @@ -465,7 +468,7 @@ func PrometheusServerMetrics(_ string) Middleware { } serverLatency.With(labels).Observe(time.Since(start).Seconds()) serverRequestSize.With(labels).Observe(float64(r.ContentLength)) - serverResponseSize.With(labels).Observe(float64(wrapped.bytesWritten)) + serverResponseSize.With(labels).Observe(float64(rec.bytesWritten)) totalRequestLabels := prometheus.Labels{ methodLabel: method, @@ -510,3 +513,50 @@ func (rr *responseRecorder) WriteHeader(code int) { rr.ResponseWriter.WriteHeader(code) rr.responseCode = code } + +// wrappers +type wrappedHijacker struct { + http.ResponseWriter + http.Hijacker +} + +type wrappedFlusher struct { + http.ResponseWriter + http.Flusher +} + +type wrappedFlushHijacker struct { + http.ResponseWriter + http.Flusher + http.Hijacker +} + +func allowFlushHijack(original, rw http.ResponseWriter) http.ResponseWriter { + flusher, isFlusher := original.(http.Flusher) + hijacker, isHijacker := original.(http.Hijacker) + switch { + case isFlusher && isHijacker: + return &wrappedFlushHijacker{rw, flusher, hijacker} + case isFlusher: + return &wrappedFlusher{rw, flusher} + case isHijacker: + return &wrappedHijacker{rw, hijacker} + default: + return rw + } + +} + +func allowFlush(original, rw http.ResponseWriter) http.ResponseWriter { + if flusher, isFlusher := original.(http.Flusher); isFlusher { + return &wrappedFlusher{rw, flusher} + } + return rw +} + +func allowHijack(original, rw http.ResponseWriter) http.ResponseWriter { + if hijacker, isHijacker := original.(http.Hijacker); isHijacker { + return &wrappedHijacker{rw, hijacker} + } + return rw +} From 49397ad66eac089724e6a934e157451438a74e72 Mon Sep 17 00:00:00 2001 From: Adam Sax <1733559+adamthesax@users.noreply.github.com> Date: Mon, 24 Oct 2022 15:56:10 -0400 Subject: [PATCH 2/9] Update httpbp/middlewares.go Co-authored-by: Kyle Lemons --- httpbp/middlewares.go | 1 - 1 file changed, 1 deletion(-) diff --git a/httpbp/middlewares.go b/httpbp/middlewares.go index 4852aa0b6..764d50c16 100644 --- a/httpbp/middlewares.go +++ b/httpbp/middlewares.go @@ -514,7 +514,6 @@ func (rr *responseRecorder) WriteHeader(code int) { rr.responseCode = code } -// wrappers type wrappedHijacker struct { http.ResponseWriter http.Hijacker From 7a442bcacc16252ce3097446c07930b6fa1e589a Mon Sep 17 00:00:00 2001 From: Adam Sax Date: Mon, 24 Oct 2022 17:16:44 -0400 Subject: [PATCH 3/9] added unit test --- httpbp/middlewares_test.go | 88 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/httpbp/middlewares_test.go b/httpbp/middlewares_test.go index 7f7d5c965..f8b00c78e 100644 --- a/httpbp/middlewares_test.go +++ b/httpbp/middlewares_test.go @@ -1,15 +1,18 @@ package httpbp_test import ( + "bufio" "context" "encoding/json" "errors" + "net" "net/http" "net/http/httptest" "sort" "strings" "testing" + "github.com/reddit/baseplate.go" "github.com/reddit/baseplate.go/ecinterface" "github.com/reddit/baseplate.go/httpbp" "github.com/reddit/baseplate.go/log" @@ -323,3 +326,88 @@ func TestSupportedMethods(t *testing.T) { ) } } + +func TestMiddlewareWrapping(t *testing.T) { + store := newSecretsStore(t) + defer store.Close() + + bp := baseplate.NewTestBaseplate(baseplate.NewTestBaseplateArgs{ + Config: baseplate.Config{Addr: ":8080"}, + Store: store, + EdgeContextImpl: ecinterface.Mock(), + }) + + args := httpbp.ServerArgs{ + Baseplate: bp, + Middlewares: []httpbp.Middleware{ + func(name string, next httpbp.HandlerFunc) httpbp.HandlerFunc { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + if flusher, isFlusher := w.(http.Flusher); isFlusher { + flusher.Flush() + } + + next(ctx, w, r) + return nil + } + }, + func(name string, next httpbp.HandlerFunc) httpbp.HandlerFunc { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + if hijacker, isHijacker := w.(http.Hijacker); isHijacker { + hijacker.Hijack() + } + + next(ctx, w, r) + return nil + } + }, + }, + Endpoints: map[httpbp.Pattern]httpbp.Endpoint{ + "/test": { + Name: "test", + Methods: []string{http.MethodGet}, + Handle: func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + w.Write([]byte("endpoint")) + return nil + }, + }, + }, + } + + // register our middleware to the EndpointRegistry + args, err := args.SetupEndpoints() + + if err != nil { + t.Fatal(err) + } + + // Test the a flushable response + t.Run("flushable response", func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/test", nil) + w := httptest.NewRecorder() + args.EndpointRegistry.ServeHTTP(w, r) + + if !w.Flushed { + t.Error("expected http response to be flushed") + } + }) + + t.Run("hijackable-response", func(tt *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/test", nil) + w := &hijackableResponseRecorder{httptest.NewRecorder(), false} + args.EndpointRegistry.ServeHTTP(w, r) + + if !w.Hijacked { + t.Error("expected http response to be hijacked") + } + }) +} + +type hijackableResponseRecorder struct { + *httptest.ResponseRecorder + Hijacked bool +} + +func (h *hijackableResponseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { + h.Hijacked = true + return nil, nil, nil +} From 08e1879feab49c5d0c1654b2ab1db01433319a4a Mon Sep 17 00:00:00 2001 From: Adam Sax Date: Mon, 24 Oct 2022 17:22:21 -0400 Subject: [PATCH 4/9] chain wrapper functions together --- httpbp/middlewares.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httpbp/middlewares.go b/httpbp/middlewares.go index 764d50c16..b0f5aa082 100644 --- a/httpbp/middlewares.go +++ b/httpbp/middlewares.go @@ -537,9 +537,9 @@ func allowFlushHijack(original, rw http.ResponseWriter) http.ResponseWriter { case isFlusher && isHijacker: return &wrappedFlushHijacker{rw, flusher, hijacker} case isFlusher: - return &wrappedFlusher{rw, flusher} + return allowFlush(original, rw) case isHijacker: - return &wrappedHijacker{rw, hijacker} + return allowHijack(original, rw) default: return rw } From 193243d7b6fd3bc79addfc751449f44faab63b93 Mon Sep 17 00:00:00 2001 From: Adam Sax Date: Mon, 24 Oct 2022 17:26:53 -0400 Subject: [PATCH 5/9] test tweaks --- httpbp/middlewares_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httpbp/middlewares_test.go b/httpbp/middlewares_test.go index f8b00c78e..6dcbff2e4 100644 --- a/httpbp/middlewares_test.go +++ b/httpbp/middlewares_test.go @@ -327,7 +327,7 @@ func TestSupportedMethods(t *testing.T) { } } -func TestMiddlewareWrapping(t *testing.T) { +func TestMiddlewareResponseWrapping(t *testing.T) { store := newSecretsStore(t) defer store.Close() @@ -381,7 +381,7 @@ func TestMiddlewareWrapping(t *testing.T) { } // Test the a flushable response - t.Run("flushable response", func(t *testing.T) { + t.Run("flushable-response", func(t *testing.T) { r := httptest.NewRequest(http.MethodGet, "/test", nil) w := httptest.NewRecorder() args.EndpointRegistry.ServeHTTP(w, r) From 5ad3f180281e2a85770a8145bb35ac2b0f1a8d2b Mon Sep 17 00:00:00 2001 From: Adam Sax Date: Tue, 25 Oct 2022 09:20:43 -0400 Subject: [PATCH 6/9] additional test cases --- httpbp/middlewares_test.go | 48 ++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/httpbp/middlewares_test.go b/httpbp/middlewares_test.go index 6dcbff2e4..01250095a 100644 --- a/httpbp/middlewares_test.go +++ b/httpbp/middlewares_test.go @@ -380,30 +380,68 @@ func TestMiddlewareResponseWrapping(t *testing.T) { t.Fatal(err) } + t.Run("non-flushable-non-hijackable", func(tt *testing.T) { + type baseResponseWriter struct { + http.ResponseWriter + } + + r := httptest.NewRequest(http.MethodGet, "/test", nil) + inner := httptest.NewRecorder() + w := baseResponseWriter{inner} + args.EndpointRegistry.ServeHTTP(w, r) + + if inner.Flushed { + tt.Error("expected response to not be flushed") + } + }) + // Test the a flushable response - t.Run("flushable-response", func(t *testing.T) { + t.Run("flushable", func(tt *testing.T) { r := httptest.NewRequest(http.MethodGet, "/test", nil) w := httptest.NewRecorder() args.EndpointRegistry.ServeHTTP(w, r) if !w.Flushed { - t.Error("expected http response to be flushed") + tt.Error("expected http response to be flushed") } }) - t.Run("hijackable-response", func(tt *testing.T) { + t.Run("hijackable", func(tt *testing.T) { r := httptest.NewRequest(http.MethodGet, "/test", nil) w := &hijackableResponseRecorder{httptest.NewRecorder(), false} args.EndpointRegistry.ServeHTTP(w, r) if !w.Hijacked { - t.Error("expected http response to be hijacked") + tt.Error("expected http response to be hijacked") + } + }) + + t.Run("hijackable-flushable", func(tt *testing.T) { + type hijackableFlushableRecorder struct { + hijackableResponseRecorder + http.Flusher + } + + r := httptest.NewRequest(http.MethodGet, "/test", nil) + inner := httptest.NewRecorder() + w := &hijackableFlushableRecorder{ + hijackableResponseRecorder{inner, false}, + inner, + } + args.EndpointRegistry.ServeHTTP(w, r) + + if !w.Hijacked { + tt.Error("expected http response to be hijacked") + } + + if !inner.Flushed { + tt.Error("expected http response to be flushed") } }) } type hijackableResponseRecorder struct { - *httptest.ResponseRecorder + http.ResponseWriter Hijacked bool } From f3f28de8e4e9ccc5cc37ae4bdc45149d25c362df Mon Sep 17 00:00:00 2001 From: Adam Sax Date: Wed, 26 Oct 2022 10:42:17 -0400 Subject: [PATCH 7/9] update to use jump table and support http.Pusher --- httpbp/middlewares.go | 50 ++----------------------------------- httpbp/middlewares_test.go | 30 ++++++++++++++++++++++ httpbp/response_wrappers.go | 33 ++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 48 deletions(-) create mode 100644 httpbp/response_wrappers.go diff --git a/httpbp/middlewares.go b/httpbp/middlewares.go index b0f5aa082..e7ae8f222 100644 --- a/httpbp/middlewares.go +++ b/httpbp/middlewares.go @@ -395,7 +395,7 @@ func recordStatusCode(counters counterGenerator) Middleware { counter := counters.Counter("baseplate.http." + name + ".response") return func(ctx context.Context, w http.ResponseWriter, r *http.Request) (err error) { rec := &statusCodeRecorder{ResponseWriter: w} - wrapped := allowFlushHijack(w, rec) + wrapped := wrapResponseWriter(w, rec) defer func() { code := rec.getCode(err) counter.With("status", statusCodeFamily(code)).Add(1) @@ -455,7 +455,7 @@ func PrometheusServerMetrics(_ string) Middleware { serverActiveRequests.With(activeRequestLabels).Inc() rec := &responseRecorder{ResponseWriter: w} - wrapped := allowFlushHijack(w, rec) + wrapped := wrapResponseWriter(w, rec) defer func() { code := errorCodeForMetrics(rec.responseCode, err) @@ -513,49 +513,3 @@ func (rr *responseRecorder) WriteHeader(code int) { rr.ResponseWriter.WriteHeader(code) rr.responseCode = code } - -type wrappedHijacker struct { - http.ResponseWriter - http.Hijacker -} - -type wrappedFlusher struct { - http.ResponseWriter - http.Flusher -} - -type wrappedFlushHijacker struct { - http.ResponseWriter - http.Flusher - http.Hijacker -} - -func allowFlushHijack(original, rw http.ResponseWriter) http.ResponseWriter { - flusher, isFlusher := original.(http.Flusher) - hijacker, isHijacker := original.(http.Hijacker) - switch { - case isFlusher && isHijacker: - return &wrappedFlushHijacker{rw, flusher, hijacker} - case isFlusher: - return allowFlush(original, rw) - case isHijacker: - return allowHijack(original, rw) - default: - return rw - } - -} - -func allowFlush(original, rw http.ResponseWriter) http.ResponseWriter { - if flusher, isFlusher := original.(http.Flusher); isFlusher { - return &wrappedFlusher{rw, flusher} - } - return rw -} - -func allowHijack(original, rw http.ResponseWriter) http.ResponseWriter { - if hijacker, isHijacker := original.(http.Hijacker); isHijacker { - return &wrappedHijacker{rw, hijacker} - } - return rw -} diff --git a/httpbp/middlewares_test.go b/httpbp/middlewares_test.go index 01250095a..03b00406d 100644 --- a/httpbp/middlewares_test.go +++ b/httpbp/middlewares_test.go @@ -356,6 +356,16 @@ func TestMiddlewareResponseWrapping(t *testing.T) { hijacker.Hijack() } + next(ctx, w, r) + return nil + } + }, + func(name string, next httpbp.HandlerFunc) httpbp.HandlerFunc { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + if pusher, isPusher := w.(http.Pusher); isPusher { + pusher.Push("target", &http.PushOptions{}) + } + next(ctx, w, r) return nil } @@ -416,6 +426,16 @@ func TestMiddlewareResponseWrapping(t *testing.T) { } }) + t.Run("pushable", func(tt *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/test", nil) + w := &pushableResponseRecorder{httptest.NewRecorder(), false} + args.EndpointRegistry.ServeHTTP(w, r) + + if !w.Pushed { + tt.Error("expected http response to be pushed") + } + }) + t.Run("hijackable-flushable", func(tt *testing.T) { type hijackableFlushableRecorder struct { hijackableResponseRecorder @@ -449,3 +469,13 @@ func (h *hijackableResponseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, erro h.Hijacked = true return nil, nil, nil } + +type pushableResponseRecorder struct { + http.ResponseWriter + Pushed bool +} + +func (p *pushableResponseRecorder) Push(target string, opts *http.PushOptions) error { + p.Pushed = true + return nil +} diff --git a/httpbp/response_wrappers.go b/httpbp/response_wrappers.go new file mode 100644 index 000000000..e3439f858 --- /dev/null +++ b/httpbp/response_wrappers.go @@ -0,0 +1,33 @@ +// DO NOT EDIT. +// This code was partially generated and then made human readable. +// This approach was adapted from https://github.com/badgerodon/contextaware/blob/4c442dfd39106512496bdd13c42c451da8ddeff3/internal/generate-wrap/main.go +// See also https://www.doxsey.net/blog/fixing-interface-erasure-in-go/ for more context + +package httpbp + +import ( + "net/http" +) + +func wrapResponseWriter(orig, wrapped http.ResponseWriter) http.ResponseWriter { + var f uint64 + flusher, isFlusher := orig.(http.Flusher) + if isFlusher { f |= 0x0001 } + hijacker, isHijacker := orig.(http.Hijacker) + if isHijacker { f |= 0x0002 } + pusher, isPusher := orig.(http.Pusher) + if isPusher { f |= 0x0004 } + + switch f { + case 0x0000: return wrapped + case 0x0001: return struct{http.ResponseWriter;http.Flusher}{wrapped, flusher} + case 0x0002: return struct{http.ResponseWriter;http.Hijacker}{wrapped, hijacker} + case 0x0003: return struct{http.ResponseWriter;http.Flusher;http.Hijacker}{wrapped, flusher,hijacker} + case 0x0004: return struct{http.ResponseWriter;http.Pusher}{wrapped, pusher} + case 0x0005: return struct{http.ResponseWriter;http.Flusher;http.Pusher}{wrapped,flusher,pusher} + case 0x0006: return struct{http.ResponseWriter;http.Hijacker;http.Pusher}{wrapped, hijacker,pusher} + case 0x0007: return struct{http.ResponseWriter;http.Flusher;http.Hijacker;http.Pusher}{wrapped, flusher,hijacker,pusher} + } + + return wrapped +} From 574ce14d341f196870d858269e9078e82cc84ec2 Mon Sep 17 00:00:00 2001 From: Adam Sax Date: Mon, 31 Oct 2022 11:53:21 -0400 Subject: [PATCH 8/9] remove generated code and write all wrappers by hand --- httpbp/response_wrappers.go | 83 ++++++++++++++++++++++++++++--------- 1 file changed, 63 insertions(+), 20 deletions(-) diff --git a/httpbp/response_wrappers.go b/httpbp/response_wrappers.go index e3439f858..88dd6d5fe 100644 --- a/httpbp/response_wrappers.go +++ b/httpbp/response_wrappers.go @@ -1,32 +1,75 @@ -// DO NOT EDIT. -// This code was partially generated and then made human readable. -// This approach was adapted from https://github.com/badgerodon/contextaware/blob/4c442dfd39106512496bdd13c42c451da8ddeff3/internal/generate-wrap/main.go -// See also https://www.doxsey.net/blog/fixing-interface-erasure-in-go/ for more context - package httpbp import ( "net/http" ) +type optionalResponseWriter uint64 + +const ( + flusher optionalResponseWriter = 1 << iota + hijacker + pusher +) + func wrapResponseWriter(orig, wrapped http.ResponseWriter) http.ResponseWriter { - var f uint64 - flusher, isFlusher := orig.(http.Flusher) - if isFlusher { f |= 0x0001 } - hijacker, isHijacker := orig.(http.Hijacker) - if isHijacker { f |= 0x0002 } - pusher, isPusher := orig.(http.Pusher) - if isPusher { f |= 0x0004 } + var f optionalResponseWriter + fl, isFlusher := orig.(http.Flusher) + if isFlusher { + f |= flusher + } + h, isHijacker := orig.(http.Hijacker) + if isHijacker { + f |= hijacker + } + p, isPusher := orig.(http.Pusher) + if isPusher { + f |= pusher + } switch f { - case 0x0000: return wrapped - case 0x0001: return struct{http.ResponseWriter;http.Flusher}{wrapped, flusher} - case 0x0002: return struct{http.ResponseWriter;http.Hijacker}{wrapped, hijacker} - case 0x0003: return struct{http.ResponseWriter;http.Flusher;http.Hijacker}{wrapped, flusher,hijacker} - case 0x0004: return struct{http.ResponseWriter;http.Pusher}{wrapped, pusher} - case 0x0005: return struct{http.ResponseWriter;http.Flusher;http.Pusher}{wrapped,flusher,pusher} - case 0x0006: return struct{http.ResponseWriter;http.Hijacker;http.Pusher}{wrapped, hijacker,pusher} - case 0x0007: return struct{http.ResponseWriter;http.Flusher;http.Hijacker;http.Pusher}{wrapped, flusher,hijacker,pusher} + case 0: + return wrapped + case flusher: + return struct { + http.ResponseWriter + http.Flusher + }{wrapped, fl} + case hijacker: + return struct { + http.ResponseWriter + http.Hijacker + }{wrapped, h} + case flusher | hijacker: + return struct { + http.ResponseWriter + http.Flusher + http.Hijacker + }{wrapped, fl, h} + case pusher: + return struct { + http.ResponseWriter + http.Pusher + }{wrapped, p} + case flusher | pusher: + return struct { + http.ResponseWriter + http.Flusher + http.Pusher + }{wrapped, fl, p} + case hijacker | pusher: + return struct { + http.ResponseWriter + http.Hijacker + http.Pusher + }{wrapped, h, p} + case flusher | hijacker | pusher: + return struct { + http.ResponseWriter + http.Flusher + http.Hijacker + http.Pusher + }{wrapped, fl, h, p} } return wrapped From 022d7f115df861925f78121aa223743e37ebe7b8 Mon Sep 17 00:00:00 2001 From: Adam Sax Date: Mon, 31 Oct 2022 12:58:53 -0400 Subject: [PATCH 9/9] PR feedback --- httpbp/response_wrappers.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/httpbp/response_wrappers.go b/httpbp/response_wrappers.go index 88dd6d5fe..8d6b417b7 100644 --- a/httpbp/response_wrappers.go +++ b/httpbp/response_wrappers.go @@ -13,28 +13,28 @@ const ( ) func wrapResponseWriter(orig, wrapped http.ResponseWriter) http.ResponseWriter { - var f optionalResponseWriter - fl, isFlusher := orig.(http.Flusher) + var w optionalResponseWriter + f, isFlusher := orig.(http.Flusher) if isFlusher { - f |= flusher + w |= flusher } h, isHijacker := orig.(http.Hijacker) if isHijacker { - f |= hijacker + w |= hijacker } p, isPusher := orig.(http.Pusher) if isPusher { - f |= pusher + w |= pusher } - switch f { + switch w { case 0: return wrapped case flusher: return struct { http.ResponseWriter http.Flusher - }{wrapped, fl} + }{wrapped, f} case hijacker: return struct { http.ResponseWriter @@ -45,7 +45,7 @@ func wrapResponseWriter(orig, wrapped http.ResponseWriter) http.ResponseWriter { http.ResponseWriter http.Flusher http.Hijacker - }{wrapped, fl, h} + }{wrapped, f, h} case pusher: return struct { http.ResponseWriter @@ -56,7 +56,7 @@ func wrapResponseWriter(orig, wrapped http.ResponseWriter) http.ResponseWriter { http.ResponseWriter http.Flusher http.Pusher - }{wrapped, fl, p} + }{wrapped, f, p} case hijacker | pusher: return struct { http.ResponseWriter @@ -69,7 +69,7 @@ func wrapResponseWriter(orig, wrapped http.ResponseWriter) http.ResponseWriter { http.Flusher http.Hijacker http.Pusher - }{wrapped, fl, h, p} + }{wrapped, f, h, p} } return wrapped