Skip to content

Commit af70a0e

Browse files
Always close typhon response body when present (#179)
1 parent 8b5d72a commit af70a0e

2 files changed

Lines changed: 72 additions & 19 deletions

File tree

e2e_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -877,3 +877,50 @@ func TestE2EMaxConnectionAge_ConnectionAgeSet(t *testing.T) {
877877
require.NotEmpty(t, connectionStart)
878878
})
879879
}
880+
881+
type closeTrackingReadCloser struct {
882+
inner io.ReadCloser
883+
isClosed *bool
884+
}
885+
886+
func (c *closeTrackingReadCloser) Read(p []byte) (n int, err error) {
887+
return c.inner.Read(p)
888+
}
889+
890+
func (c *closeTrackingReadCloser) Close() error {
891+
*c.isClosed = true
892+
return c.inner.Close()
893+
}
894+
895+
func TestE2EStatusCodesWithoutAResponseBody(t *testing.T) {
896+
flavours(t, func(t *testing.T, flav e2eFlavour) {
897+
ctx, cancel := flav.Context()
898+
defer cancel()
899+
900+
responseBodyWasClosed := false
901+
902+
svc := Service(func(req Request) Response {
903+
response := req.Response([]byte("hello"))
904+
response.Header.Set("content-type", "text/plain")
905+
response.StatusCode = http.StatusNotModified // Not Modified
906+
oldBody := response.Body
907+
response.Body = &closeTrackingReadCloser{
908+
inner: oldBody,
909+
isClosed: &responseBodyWasClosed,
910+
}
911+
return response
912+
})
913+
svc = svc.Filter(ErrorFilter)
914+
s := flav.Serve(svc)
915+
defer s.Stop(context.Background())
916+
917+
req := NewRequest(ctx, "GET", flav.URL(s), nil)
918+
rsp := req.Send().Response()
919+
require.NoError(t, rsp.Error)
920+
assert.Equal(t, http.StatusNotModified, rsp.StatusCode)
921+
rspBody, err := rsp.BodyBytes(true)
922+
require.NoError(t, err)
923+
assert.Empty(t, rspBody)
924+
assert.True(t, responseBodyWasClosed)
925+
})
926+
}

http.go

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -117,34 +117,40 @@ func HttpHandler(svc Service) http.Handler {
117117
}
118118
rsp := svc(req)
119119

120-
// If the connection was hijacked, we should not attempt to write anything out
121120
if rsp.hijacked {
122121
return
123122
}
124123

124+
// If the connection has been hijacked, the hijacker is responsible for any
125+
// resource cleanup (as per http.Hijacker)
126+
if rsp.Body != nil {
127+
defer rsp.Body.Close()
128+
}
129+
125130
rwHeader := rw.Header()
126131
for k, v := range rsp.Header {
127132
rwHeader[k] = v
128133
}
129134
rw.WriteHeader(rsp.StatusCode)
130-
if rsp.Body != nil && bodyAllowedForStatus(rsp.StatusCode) {
131-
defer rsp.Body.Close()
132-
buf := *httpChunkBufPool.Get().(*[]byte)
133-
defer httpChunkBufPool.Put(&buf)
134-
if isStreamingRsp(rsp) {
135-
// Streaming responses use copyChunked(), which takes care of flushing transparently
136-
if _, err := copyChunked(rw, rsp.Body, buf); err != nil {
137-
slog.Log(slog.Eventf(copyErrSeverity(err), req, "Couldn't send streaming response body", err))
138-
139-
// Prevent the client from accidentally consuming a truncated stream by aborting the response.
140-
// The official way of interrupting an HTTP reply mid-stream is panic(http.ErrAbortHandler), which
141-
// works for both HTTP/1.1 and HTTP.2. https://github.com/golang/go/issues/17790
142-
panic(http.ErrAbortHandler)
143-
}
144-
} else {
145-
if _, err := io.CopyBuffer(rw, rsp.Body, buf); err != nil {
146-
slog.Log(slog.Eventf(copyErrSeverity(err), req, "Couldn't send response body", err))
147-
}
135+
if rsp.Body == nil || !bodyAllowedForStatus(rsp.StatusCode) {
136+
return
137+
}
138+
139+
buf := *httpChunkBufPool.Get().(*[]byte)
140+
defer httpChunkBufPool.Put(&buf)
141+
if isStreamingRsp(rsp) {
142+
// Streaming responses use copyChunked(), which takes care of flushing transparently
143+
if _, err := copyChunked(rw, rsp.Body, buf); err != nil {
144+
slog.Log(slog.Eventf(copyErrSeverity(err), req, "Couldn't send streaming response body", err))
145+
146+
// Prevent the client from accidentally consuming a truncated stream by aborting the response.
147+
// The official way of interrupting an HTTP reply mid-stream is panic(http.ErrAbortHandler), which
148+
// works for both HTTP/1.1 and HTTP.2. https://github.com/golang/go/issues/17790
149+
panic(http.ErrAbortHandler)
150+
}
151+
} else {
152+
if _, err := io.CopyBuffer(rw, rsp.Body, buf); err != nil {
153+
slog.Log(slog.Eventf(copyErrSeverity(err), req, "Couldn't send response body", err))
148154
}
149155
}
150156
})

0 commit comments

Comments
 (0)