Skip to content

Commit 12ed72e

Browse files
author
huozb
committed
net/http: fix request canceler leak on http.Transport
A write error in pconn.writeLoop when channel waitForBodyRead in pconn.readLoop is still blocking will cause the request canceler never be removed. This would cause memory leak in http.Transport. Note that the write error could be either "http: invalid Read on closed" (issue 61708 case, req body was close by (*chunkWriter).writeHeader src/net/http/server.go:1405 while (*persistConn).writeLoop is still reading it. A reverseproxy specific problem.) or "http: ContentLength=1048576 with Body length 1" (PR unit test case). Without the fix: pending requests = 5; want 0. With the fix: ALL TESTS PASSED. Thanks the previous works done by @AlexanderYastrebov. Fixes #61708
1 parent 789b3f8 commit 12ed72e

File tree

2 files changed

+77
-0
lines changed

2 files changed

+77
-0
lines changed

src/net/http/transport.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2267,6 +2267,7 @@ func (pc *persistConn) readLoop() {
22672267
pc.t.cancelRequest(rc.cancelKey, rc.req.Context().Err())
22682268
case <-pc.closech:
22692269
alive = false
2270+
pc.t.setReqCanceler(rc.cancelKey, nil)
22702271
}
22712272

22722273
testHookReadLoopBeforeNextRead()

src/net/http/transport_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6810,3 +6810,79 @@ func testRequestSanitization(t *testing.T, mode testMode) {
68106810
resp.Body.Close()
68116811
}
68126812
}
6813+
6814+
// chunkedReader is an io.Reader that reads one chunk at a time
6815+
// and adds a delay between each read operation.
6816+
// When the channel is closed, the reader returns io.EOF.
6817+
type chunkedReader struct {
6818+
data [][]byte
6819+
index int
6820+
delay time.Duration
6821+
}
6822+
6823+
func (cr *chunkedReader) Read(p []byte) (int, error) {
6824+
if cr.index >= len(cr.data) {
6825+
return 0, io.EOF
6826+
}
6827+
6828+
n := copy(p, cr.data[cr.index])
6829+
cr.index++
6830+
6831+
time.Sleep(cr.delay)
6832+
return n, nil
6833+
}
6834+
6835+
// Issue 61708
6836+
// Test that a write error from writeLoop should not cause request leak
6837+
// in transport.reqCanceler.
6838+
func TestHttp1TransportReqCancelerLeakOnWriteLoopError(t *testing.T) {
6839+
defer afterTest(t)
6840+
6841+
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, req *Request) {
6842+
w.Header().Set("Content-Type", "text/plain")
6843+
w.Header().Set("Content-Length", "1")
6844+
w.WriteHeader(StatusOK)
6845+
w.Write([]byte("x"))
6846+
w.(Flusher).Flush()
6847+
}))
6848+
defer ts.Close()
6849+
6850+
c := ts.Client()
6851+
tr := c.Transport.(*Transport)
6852+
6853+
// reqBodySize should be large enough.
6854+
const reqBodySize = 1 << 20
6855+
const repeat = 5
6856+
reqData := [][]byte{
6857+
[]byte("x"),
6858+
}
6859+
6860+
for i := 0; i < repeat; i++ {
6861+
cr := &chunkedReader{data: reqData, delay: time.Millisecond * 2}
6862+
req, _ := NewRequest("POST", ts.URL, cr)
6863+
req.ContentLength = reqBodySize
6864+
6865+
resp, _ := tr.RoundTrip(req)
6866+
6867+
// Wait a little bit more time than read delay so that
6868+
// pc.closech can be received before waitForBodyRead in readLoop.
6869+
time.Sleep(time.Millisecond * 10)
6870+
req.Body.Close()
6871+
if resp != nil {
6872+
resp.Body.Close()
6873+
}
6874+
}
6875+
6876+
// Verify no outstanding requests after readLoop/writeLoop
6877+
// goroutines shut down.
6878+
for tries := 5; tries > 0; tries-- {
6879+
n := tr.NumPendingRequestsForTesting()
6880+
if n == 0 {
6881+
break
6882+
}
6883+
time.Sleep(100 * time.Millisecond)
6884+
if tries == 1 {
6885+
t.Errorf("pending requests = %d; want 0", n)
6886+
}
6887+
}
6888+
}

0 commit comments

Comments
 (0)