Skip to content

Commit a84ddfe

Browse files
authored
use httpsnoop to ensure http.ResponseWriter additional interfaces are preserved (#388)
* add a test to make sure the recordingResponseWriter preserves interfaces implemented by the wrapped writer * use httpsnoop to wrap the response writer so we don't lose interfaces * don't bother checking for deprecated interfaces * run precommit
1 parent bbc03fa commit a84ddfe

File tree

5 files changed

+85
-23
lines changed

5 files changed

+85
-23
lines changed

instrumentation/github.com/gorilla/mux/otelmux/example/go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
1313
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
1414
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
1515
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
16+
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
17+
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
1618
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
1719
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
1820
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=

instrumentation/github.com/gorilla/mux/otelmux/go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ replace (
88
)
99

1010
require (
11+
github.com/felixge/httpsnoop v1.0.1
1112
github.com/gorilla/mux v1.8.0
1213
github.com/stretchr/testify v1.6.1
1314
go.opentelemetry.io/contrib v0.12.0

instrumentation/github.com/gorilla/mux/otelmux/go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
22
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
33
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
5+
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
46
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
57
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
68
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=

instrumentation/github.com/gorilla/mux/otelmux/mux.go

+22-23
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"net/http"
2020
"sync"
2121

22+
"github.com/felixge/httpsnoop"
2223
"github.com/gorilla/mux"
2324

2425
otelcontrib "go.opentelemetry.io/contrib"
@@ -74,27 +75,6 @@ type recordingResponseWriter struct {
7475
status int
7576
}
7677

77-
func (w *recordingResponseWriter) Header() http.Header {
78-
return w.writer.Header()
79-
}
80-
81-
func (w *recordingResponseWriter) Write(slice []byte) (int, error) {
82-
w.writeHeader(http.StatusOK)
83-
return w.writer.Write(slice)
84-
}
85-
86-
func (w *recordingResponseWriter) WriteHeader(statusCode int) {
87-
w.writeHeader(statusCode)
88-
w.writer.WriteHeader(statusCode)
89-
}
90-
91-
func (w *recordingResponseWriter) writeHeader(statusCode int) {
92-
if !w.written {
93-
w.written = true
94-
w.status = statusCode
95-
}
96-
}
97-
9878
var rrwPool = &sync.Pool{
9979
New: func() interface{} {
10080
return &recordingResponseWriter{}
@@ -105,7 +85,26 @@ func getRRW(writer http.ResponseWriter) *recordingResponseWriter {
10585
rrw := rrwPool.Get().(*recordingResponseWriter)
10686
rrw.written = false
10787
rrw.status = 0
108-
rrw.writer = writer
88+
rrw.writer = httpsnoop.Wrap(writer, httpsnoop.Hooks{
89+
Write: func(next httpsnoop.WriteFunc) httpsnoop.WriteFunc {
90+
return func(b []byte) (int, error) {
91+
if !rrw.written {
92+
rrw.written = true
93+
rrw.status = http.StatusOK
94+
}
95+
return next(b)
96+
}
97+
},
98+
WriteHeader: func(next httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc {
99+
return func(statusCode int) {
100+
if !rrw.written {
101+
rrw.written = true
102+
rrw.status = statusCode
103+
}
104+
next(statusCode)
105+
}
106+
},
107+
})
109108
return rrw
110109
}
111110

@@ -145,7 +144,7 @@ func (tw traceware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
145144
r2 := r.WithContext(ctx)
146145
rrw := getRRW(w)
147146
defer putRRW(rrw)
148-
tw.handler.ServeHTTP(rrw, r2)
147+
tw.handler.ServeHTTP(rrw.writer, r2)
149148
attrs := semconv.HTTPAttributesFromHTTPStatusCode(rrw.status)
150149
spanStatus, spanMessage := semconv.SpanStatusFromHTTPStatusCode(rrw.status)
151150
span.SetAttributes(attrs...)

instrumentation/github.com/gorilla/mux/otelmux/mux_test.go

+58
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
package otelmux
1616

1717
import (
18+
"bufio"
1819
"context"
20+
"io"
21+
"net"
1922
"net/http"
2023
"net/http/httptest"
2124
"testing"
@@ -183,3 +186,58 @@ func TestPropagationWithCustomPropagators(t *testing.T) {
183186

184187
router.ServeHTTP(w, r)
185188
}
189+
190+
type testResponseWriter struct {
191+
writer http.ResponseWriter
192+
}
193+
194+
func (rw *testResponseWriter) Header() http.Header {
195+
return rw.writer.Header()
196+
}
197+
func (rw *testResponseWriter) Write(b []byte) (int, error) {
198+
return rw.writer.Write(b)
199+
}
200+
func (rw *testResponseWriter) WriteHeader(statusCode int) {
201+
rw.writer.WriteHeader(statusCode)
202+
}
203+
204+
// implement Hijacker
205+
func (rw *testResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
206+
return nil, nil, nil
207+
}
208+
209+
// implement Pusher
210+
func (rw *testResponseWriter) Push(target string, opts *http.PushOptions) error {
211+
return nil
212+
}
213+
214+
// implement Flusher
215+
func (rw *testResponseWriter) Flush() {
216+
}
217+
218+
// implement io.ReaderFrom
219+
func (rw *testResponseWriter) ReadFrom(r io.Reader) (n int64, err error) {
220+
return 0, nil
221+
}
222+
223+
func TestResponseWriterInterfaces(t *testing.T) {
224+
// make sure the recordingResponseWriter preserves interfaces implemented by the wrapped writer
225+
provider, _ := mocktrace.NewTracerProviderAndTracer(tracerName)
226+
227+
router := mux.NewRouter()
228+
router.Use(Middleware("foobar", WithTracerProvider(provider)))
229+
router.HandleFunc("/user/{id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
230+
assert.Implements(t, (*http.Hijacker)(nil), w)
231+
assert.Implements(t, (*http.Pusher)(nil), w)
232+
assert.Implements(t, (*http.Flusher)(nil), w)
233+
assert.Implements(t, (*io.ReaderFrom)(nil), w)
234+
w.WriteHeader(http.StatusOK)
235+
}))
236+
237+
r := httptest.NewRequest("GET", "/user/123", nil)
238+
w := &testResponseWriter{
239+
writer: httptest.NewRecorder(),
240+
}
241+
242+
router.ServeHTTP(w, r)
243+
}

0 commit comments

Comments
 (0)