Skip to content

Commit 7bf0baf

Browse files
committed
http2: close request body after early RoundTrip failures
The RoundTrip contract requires that the request Body be closed, even when an error occurs sending the request. Fix several cases where the body was not closed by hoisting the Close call to Transport.RoundTripOpt. Now ClientConn.roundTrip takes responsibility for closing the body once the body write begins; otherwise, the caller does so. Fix the case where a new body is acquired via Request.GetBody to close the previous body, matching net/http's behavior. Fixes golang/go#48341. Change-Id: Id9dc682d4d86a1c255c7c0d864208ff76ed53eb2 Reviewed-on: https://go-review.googlesource.com/c/net/+/349489 Trust: Damien Neil <[email protected]> Run-TryBot: Damien Neil <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent a5e0955 commit 7bf0baf

File tree

2 files changed

+51
-7
lines changed

2 files changed

+51
-7
lines changed

http2/transport.go

+10-3
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,7 @@ func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Res
490490
}
491491
reused := !atomic.CompareAndSwapUint32(&cc.reused, 0, 1)
492492
traceGotConn(req, cc, reused)
493+
body := req.Body
493494
res, gotErrAfterReqBodyWrite, err := cc.roundTrip(req)
494495
if err != nil && retry <= 6 {
495496
if req, err = shouldRetryRequest(req, err, gotErrAfterReqBodyWrite); err == nil {
@@ -503,12 +504,17 @@ func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Res
503504
case <-time.After(time.Second * time.Duration(backoff)):
504505
continue
505506
case <-req.Context().Done():
506-
return nil, req.Context().Err()
507+
err = req.Context().Err()
507508
}
508509
}
509510
}
510511
if err != nil {
511512
t.vlogf("RoundTrip failure: %v", err)
513+
// If the error occurred after the body write started,
514+
// the body writer will close the body. Otherwise, do so here.
515+
if body != nil && !gotErrAfterReqBodyWrite {
516+
body.Close()
517+
}
512518
return nil, err
513519
}
514520
return res, nil
@@ -547,7 +553,7 @@ func shouldRetryRequest(req *http.Request, err error, afterBodyWrite bool) (*htt
547553
// If the request body can be reset back to its original
548554
// state via the optional req.GetBody, do that.
549555
if req.GetBody != nil {
550-
// TODO: consider a req.Body.Close here? or audit that all caller paths do?
556+
req.Body.Close()
551557
body, err := req.GetBody()
552558
if err != nil {
553559
return nil, err
@@ -1085,13 +1091,14 @@ func (cc *ClientConn) roundTrip(req *http.Request) (res *http.Response, gotErrAf
10851091

10861092
if werr != nil {
10871093
if hasBody {
1088-
req.Body.Close() // per RoundTripper contract
10891094
bodyWriter.cancel()
10901095
}
10911096
cc.forgetStreamID(cs.ID)
10921097
// Don't bother sending a RST_STREAM (our write already failed;
10931098
// no need to keep writing)
10941099
traceWroteRequest(cs.trace, werr)
1100+
// TODO(dneil): An error occurred while writing the headers.
1101+
// Should we return an error indicating that this request can be retried?
10951102
return nil, false, werr
10961103
}
10971104

http2/transport_test.go

+41-4
Original file line numberDiff line numberDiff line change
@@ -3905,7 +3905,8 @@ func TestTransportRequestsStallAtServerLimit(t *testing.T) {
39053905
if k >= maxConcurrent {
39063906
<-unblockClient
39073907
}
3908-
req, _ := http.NewRequest("GET", fmt.Sprintf("https://dummy.tld/%d", k), nil)
3908+
body := newStaticCloseChecker("")
3909+
req, _ := http.NewRequest("GET", fmt.Sprintf("https://dummy.tld/%d", k), body)
39093910
if k == maxConcurrent {
39103911
// This request will be canceled.
39113912
cancel := make(chan struct{})
@@ -3930,6 +3931,9 @@ func TestTransportRequestsStallAtServerLimit(t *testing.T) {
39303931
return
39313932
}
39323933
}
3934+
if err := body.isClosed(); err != nil {
3935+
errs <- fmt.Errorf("RoundTrip(%d): %v", k, err)
3936+
}
39333937
}(k)
39343938
}
39353939
return nil
@@ -3990,6 +3994,7 @@ func TestTransportRequestsStallAtServerLimit(t *testing.T) {
39903994
if nreq == maxConcurrent+1 {
39913995
close(writeResp)
39923996
}
3997+
case *DataFrame:
39933998
default:
39943999
return fmt.Errorf("Unexpected client frame %v", f)
39954000
}
@@ -4905,11 +4910,41 @@ type closeChecker struct {
49054910
closed chan struct{}
49064911
}
49074912

4913+
func newCloseChecker(r io.ReadCloser) *closeChecker {
4914+
return &closeChecker{r, make(chan struct{})}
4915+
}
4916+
4917+
func newStaticCloseChecker(body string) *closeChecker {
4918+
return newCloseChecker(io.NopCloser(strings.NewReader("body")))
4919+
}
4920+
4921+
func (rc *closeChecker) Read(b []byte) (n int, err error) {
4922+
select {
4923+
default:
4924+
case <-rc.closed:
4925+
panic("read from closed body")
4926+
}
4927+
return rc.ReadCloser.Read(b)
4928+
}
4929+
49084930
func (rc *closeChecker) Close() error {
49094931
close(rc.closed)
49104932
return rc.ReadCloser.Close()
49114933
}
49124934

4935+
func (rc *closeChecker) isClosed() error {
4936+
// The RoundTrip contract says that it will close the request body,
4937+
// but that it may do so in a separate goroutine. Wait a reasonable
4938+
// amount of time before concluding that the body isn't being closed.
4939+
timeout := time.Duration(10 * time.Second)
4940+
select {
4941+
case <-rc.closed:
4942+
case <-time.After(timeout):
4943+
return fmt.Errorf("body not closed after %v", timeout)
4944+
}
4945+
return nil
4946+
}
4947+
49134948
func TestTransportCloseRequestBody(t *testing.T) {
49144949
var statusCode int
49154950
st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
@@ -4929,8 +4964,8 @@ func TestTransportCloseRequestBody(t *testing.T) {
49294964
t.Run(fmt.Sprintf("status=%d", status), func(t *testing.T) {
49304965
statusCode = status
49314966
pr, pw := io.Pipe()
4932-
pipeClosed := make(chan struct{})
4933-
req, err := http.NewRequest("PUT", "https://dummy.tld/", &closeChecker{pr, pipeClosed})
4967+
body := newCloseChecker(pr)
4968+
req, err := http.NewRequest("PUT", "https://dummy.tld/", body)
49344969
if err != nil {
49354970
t.Fatal(err)
49364971
}
@@ -4940,7 +4975,9 @@ func TestTransportCloseRequestBody(t *testing.T) {
49404975
}
49414976
res.Body.Close()
49424977
pw.Close()
4943-
<-pipeClosed
4978+
if err := body.isClosed(); err != nil {
4979+
t.Fatal(err)
4980+
}
49444981
})
49454982
}
49464983
}

0 commit comments

Comments
 (0)