Skip to content

Commit d213170

Browse files
stevenhBryan C. Mills
authored and
Bryan C. Mills
committed
net/http/httputil: fix deadlock in DumpRequestOut
Fix a deadlock in DumpRequestOut which can occur if the request is cancelled between response being sent and it being processed. Also: * Ensure we don't get a reader leak when an error is reported by the transport before the body is consumed. * Add leaked goroutine retries to avoid false test failures. Fixes #38352 Change-Id: I83710791b2985b997f61fe5b49eadee0bb51bdee Reviewed-on: https://go-review.googlesource.com/c/go/+/232798 Reviewed-by: Bryan C. Mills <[email protected]> Run-TryBot: Bryan C. Mills <[email protected]> TryBot-Result: Go Bot <[email protected]> Trust: Damien Neil <[email protected]>
1 parent 3e1e13c commit d213170

File tree

2 files changed

+87
-8
lines changed

2 files changed

+87
-8
lines changed

src/net/http/httputil/dump.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ func DumpRequestOut(req *http.Request, body bool) ([]byte, error) {
138138
select {
139139
case dr.c <- strings.NewReader("HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n"):
140140
case <-quitReadCh:
141+
// Ensure delegateReader.Read doesn't block forever if we get an error.
142+
close(dr.c)
141143
}
142144
}()
143145

@@ -146,7 +148,8 @@ func DumpRequestOut(req *http.Request, body bool) ([]byte, error) {
146148
req.Body = save
147149
if err != nil {
148150
pw.Close()
149-
quitReadCh <- struct{}{}
151+
dr.err = err
152+
close(quitReadCh)
150153
return nil, err
151154
}
152155
dump := buf.Bytes()
@@ -167,13 +170,17 @@ func DumpRequestOut(req *http.Request, body bool) ([]byte, error) {
167170
// delegateReader is a reader that delegates to another reader,
168171
// once it arrives on a channel.
169172
type delegateReader struct {
170-
c chan io.Reader
171-
r io.Reader // nil until received from c
173+
c chan io.Reader
174+
err error // only used if r is nil and c is closed.
175+
r io.Reader // nil until received from c
172176
}
173177

174178
func (r *delegateReader) Read(p []byte) (int, error) {
175179
if r.r == nil {
176-
r.r = <-r.c
180+
var ok bool
181+
if r.r, ok = <-r.c; !ok {
182+
return 0, r.err
183+
}
177184
}
178185
return r.r.Read(p)
179186
}

src/net/http/httputil/dump_test.go

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ package httputil
77
import (
88
"bufio"
99
"bytes"
10+
"context"
1011
"fmt"
1112
"io"
13+
"math/rand"
1214
"net/http"
1315
"net/url"
1416
"runtime"
17+
"runtime/pprof"
1518
"strings"
1619
"testing"
20+
"time"
1721
)
1822

1923
type eofReader struct{}
@@ -311,11 +315,39 @@ func TestDumpRequest(t *testing.T) {
311315
}
312316
}
313317
}
314-
if dg := runtime.NumGoroutine() - numg0; dg > 4 {
315-
buf := make([]byte, 4096)
316-
buf = buf[:runtime.Stack(buf, true)]
317-
t.Errorf("Unexpectedly large number of new goroutines: %d new: %s", dg, buf)
318+
319+
// Validate we haven't leaked any goroutines.
320+
var dg int
321+
dl := deadline(t, 5*time.Second, time.Second)
322+
for time.Now().Before(dl) {
323+
if dg = runtime.NumGoroutine() - numg0; dg <= 4 {
324+
// No unexpected goroutines.
325+
return
326+
}
327+
328+
// Allow goroutines to schedule and die off.
329+
runtime.Gosched()
330+
}
331+
332+
buf := make([]byte, 4096)
333+
buf = buf[:runtime.Stack(buf, true)]
334+
t.Errorf("Unexpectedly large number of new goroutines: %d new: %s", dg, buf)
335+
}
336+
337+
// deadline returns the time which is needed before t.Deadline()
338+
// if one is configured and it is s greater than needed in the future,
339+
// otherwise defaultDelay from the current time.
340+
func deadline(t *testing.T, defaultDelay, needed time.Duration) time.Time {
341+
if dl, ok := t.Deadline(); ok {
342+
if dl = dl.Add(-needed); dl.After(time.Now()) {
343+
// Allow an arbitrarily long delay.
344+
return dl
345+
}
318346
}
347+
348+
// No deadline configured or its closer than needed from now
349+
// so just use the default.
350+
return time.Now().Add(defaultDelay)
319351
}
320352

321353
func chunk(s string) string {
@@ -445,3 +477,43 @@ func TestDumpResponse(t *testing.T) {
445477
}
446478
}
447479
}
480+
481+
// Issue 38352: Check for deadlock on cancelled requests.
482+
func TestDumpRequestOutIssue38352(t *testing.T) {
483+
if testing.Short() {
484+
return
485+
}
486+
t.Parallel()
487+
488+
timeout := 10 * time.Second
489+
if deadline, ok := t.Deadline(); ok {
490+
timeout = time.Until(deadline)
491+
timeout -= time.Second * 2 // Leave 2 seconds to report failures.
492+
}
493+
for i := 0; i < 1000; i++ {
494+
delay := time.Duration(rand.Intn(5)) * time.Millisecond
495+
ctx, cancel := context.WithTimeout(context.Background(), delay)
496+
defer cancel()
497+
498+
r := bytes.NewBuffer(make([]byte, 10000))
499+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://example.com", r)
500+
if err != nil {
501+
t.Fatal(err)
502+
}
503+
504+
out := make(chan error)
505+
go func() {
506+
_, err = DumpRequestOut(req, true)
507+
out <- err
508+
}()
509+
510+
select {
511+
case <-out:
512+
case <-time.After(timeout):
513+
b := &bytes.Buffer{}
514+
fmt.Fprintf(b, "deadlock detected on iteration %d after %s with delay: %v\n", i, timeout, delay)
515+
pprof.Lookup("goroutine").WriteTo(b, 1)
516+
t.Fatal(b.String())
517+
}
518+
}
519+
}

0 commit comments

Comments
 (0)