Skip to content

Commit f315505

Browse files
committed
net/context/ctxhttp: fix case where Body could leak and not be closed
Fixes golang/go#14065 Change-Id: Ic19a0f740cddced8fb782f65cea14da383b047b1 Reviewed-on: https://go-review.googlesource.com/18802 Reviewed-by: Olivier Poitrey <[email protected]> Reviewed-by: Daniel Morsing <[email protected]> Reviewed-by: Chris Broadfoot <[email protected]> Run-TryBot: Brad Fitzpatrick <[email protected]>
1 parent 2e9cee7 commit f315505

File tree

2 files changed

+80
-0
lines changed

2 files changed

+80
-0
lines changed

context/ctxhttp/ctxhttp.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ import (
1414
"golang.org/x/net/context"
1515
)
1616

17+
func nop() {}
18+
19+
var (
20+
testHookContextDoneBeforeHeaders = nop
21+
testHookDoReturned = nop
22+
testHookDidBodyClose = nop
23+
)
24+
1725
// Do sends an HTTP request with the provided http.Client and returns an HTTP response.
1826
// If the client is nil, http.DefaultClient is used.
1927
// If the context is canceled or times out, ctx.Err() will be returned.
@@ -33,14 +41,23 @@ func Do(ctx context.Context, client *http.Client, req *http.Request) (*http.Resp
3341

3442
go func() {
3543
resp, err := client.Do(req)
44+
testHookDoReturned()
3645
result <- responseAndError{resp, err}
3746
}()
3847

3948
var resp *http.Response
4049

4150
select {
4251
case <-ctx.Done():
52+
testHookContextDoneBeforeHeaders()
4353
cancel()
54+
// Clean up after the goroutine calling client.Do:
55+
go func() {
56+
if r := <-result; r.resp != nil {
57+
testHookDidBodyClose()
58+
r.resp.Body.Close()
59+
}
60+
}()
4461
return nil, ctx.Err()
4562
case r := <-result:
4663
var err error

context/ctxhttp/ctxhttp_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ package ctxhttp
88

99
import (
1010
"io/ioutil"
11+
"net"
1112
"net/http"
1213
"net/http/httptest"
14+
"sync"
1315
"testing"
1416
"time"
1517

@@ -111,3 +113,64 @@ func doRequest(ctx context.Context) (*http.Response, error) {
111113

112114
return Get(ctx, nil, serv.URL)
113115
}
116+
117+
// golang.org/issue/14065
118+
func TestClosesResponseBodyOnCancel(t *testing.T) {
119+
defer func() { testHookContextDoneBeforeHeaders = nop }()
120+
defer func() { testHookDoReturned = nop }()
121+
defer func() { testHookDidBodyClose = nop }()
122+
123+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
124+
defer ts.Close()
125+
126+
ctx, cancel := context.WithCancel(context.Background())
127+
128+
// closed when Do enters select case <-ctx.Done()
129+
enteredDonePath := make(chan struct{})
130+
131+
testHookContextDoneBeforeHeaders = func() {
132+
close(enteredDonePath)
133+
}
134+
135+
testHookDoReturned = func() {
136+
// We now have the result (the Flush'd headers) at least,
137+
// so we can cancel the request.
138+
cancel()
139+
140+
// But block the client.Do goroutine from sending
141+
// until Do enters into the <-ctx.Done() path, since
142+
// otherwise if both channels are readable, select
143+
// picks a random one.
144+
<-enteredDonePath
145+
}
146+
147+
sawBodyClose := make(chan struct{})
148+
testHookDidBodyClose = func() { close(sawBodyClose) }
149+
150+
tr := &http.Transport{}
151+
defer tr.CloseIdleConnections()
152+
c := &http.Client{Transport: tr}
153+
req, _ := http.NewRequest("GET", ts.URL, nil)
154+
_, doErr := Do(ctx, c, req)
155+
156+
select {
157+
case <-sawBodyClose:
158+
case <-time.After(5 * time.Second):
159+
t.Fatal("timeout waiting for body to close")
160+
}
161+
162+
if doErr != ctx.Err() {
163+
t.Errorf("Do error = %v; want %v", doErr, ctx.Err())
164+
}
165+
}
166+
167+
type noteCloseConn struct {
168+
net.Conn
169+
onceClose sync.Once
170+
closefn func()
171+
}
172+
173+
func (c *noteCloseConn) Close() error {
174+
c.onceClose.Do(c.closefn)
175+
return c.Conn.Close()
176+
}

0 commit comments

Comments
 (0)