Skip to content

Commit 0d8ee35

Browse files
rscdmitshur
authored andcommitted
[release-branch.go1.14] net/http: handle body rewind in HTTP/2 connection loss better
In certain cases the HTTP/2 stack needs to resend a request. It obtains a fresh body to send by calling req.GetBody. This call was missing from the path where the HTTP/2 round tripper returns ErrSkipAltProtocol, meaning fall back to HTTP/1.1. The result was that the HTTP/1.1 fallback request was sent with no body at all. This CL changes that code path to rewind the body before falling back to HTTP/1.1. But rewinding the body is easier said than done. Some requests have no GetBody function, meaning the body can't be rewound. If we need to rewind and can't, that's an error. But if we didn't read anything, we don't need to rewind. So we have to track whether we read anything, with a new ReadCloser wrapper. That in turn requires adding to the couple places that unwrap Body values to look at the underlying implementation. This CL adds the new rewinding code in the main retry loop as well. The new rewindBody function also takes care of closing the old body before abandoning it. That was missing in the old rewind code. Thanks to Aleksandr Razumov for CL 210123 and to Jun Chen for CL 234358, both of which informed this CL. Updates #32441. Fixes #39279. Change-Id: Id183758526c087c6b179ab73cf3b61ed23a2a46a Reviewed-on: https://go-review.googlesource.com/c/go/+/234894 Run-TryBot: Russ Cox <[email protected]> Reviewed-by: Damien Neil <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]> (cherry picked from commit e3491c4) Reviewed-on: https://go-review.googlesource.com/c/go/+/242117 Run-TryBot: Dmitri Shuralyov <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Emmanuel Odeke <[email protected]>
1 parent 7456a46 commit 0d8ee35

File tree

3 files changed

+90
-10
lines changed

3 files changed

+90
-10
lines changed

src/net/http/transfer.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ func (t *transferWriter) writeBody(w io.Writer) error {
335335
var ncopy int64
336336

337337
// Write body. We "unwrap" the body first if it was wrapped in a
338-
// nopCloser. This is to ensure that we can take advantage of
338+
// nopCloser or readTrackingBody. This is to ensure that we can take advantage of
339339
// OS-level optimizations in the event that the body is an
340340
// *os.File.
341341
if t.Body != nil {
@@ -413,7 +413,10 @@ func (t *transferWriter) unwrapBody() io.Reader {
413413
if reflect.TypeOf(t.Body) == nopCloserType {
414414
return reflect.ValueOf(t.Body).Field(0).Interface().(io.Reader)
415415
}
416-
416+
if r, ok := t.Body.(*readTrackingBody); ok {
417+
r.didRead = true
418+
return r.ReadCloser
419+
}
417420
return t.Body
418421
}
419422

@@ -1092,6 +1095,9 @@ func isKnownInMemoryReader(r io.Reader) bool {
10921095
if reflect.TypeOf(r) == nopCloserType {
10931096
return isKnownInMemoryReader(reflect.ValueOf(r).Field(0).Interface().(io.Reader))
10941097
}
1098+
if r, ok := r.(*readTrackingBody); ok {
1099+
return isKnownInMemoryReader(r.ReadCloser)
1100+
}
10951101
return false
10961102
}
10971103

src/net/http/transport.go

+56-8
Original file line numberDiff line numberDiff line change
@@ -511,10 +511,17 @@ func (t *Transport) roundTrip(req *Request) (*Response, error) {
511511
}
512512
}
513513

514+
req = setupRewindBody(req)
515+
514516
if altRT := t.alternateRoundTripper(req); altRT != nil {
515517
if resp, err := altRT.RoundTrip(req); err != ErrSkipAltProtocol {
516518
return resp, err
517519
}
520+
var err error
521+
req, err = rewindBody(req)
522+
if err != nil {
523+
return nil, err
524+
}
518525
}
519526
if !isHTTP {
520527
req.closeBody()
@@ -587,18 +594,59 @@ func (t *Transport) roundTrip(req *Request) (*Response, error) {
587594
testHookRoundTripRetried()
588595

589596
// Rewind the body if we're able to.
590-
if req.GetBody != nil {
591-
newReq := *req
592-
var err error
593-
newReq.Body, err = req.GetBody()
594-
if err != nil {
595-
return nil, err
596-
}
597-
req = &newReq
597+
req, err = rewindBody(req)
598+
if err != nil {
599+
return nil, err
598600
}
599601
}
600602
}
601603

604+
var errCannotRewind = errors.New("net/http: cannot rewind body after connection loss")
605+
606+
type readTrackingBody struct {
607+
io.ReadCloser
608+
didRead bool
609+
}
610+
611+
func (r *readTrackingBody) Read(data []byte) (int, error) {
612+
r.didRead = true
613+
return r.ReadCloser.Read(data)
614+
}
615+
616+
// setupRewindBody returns a new request with a custom body wrapper
617+
// that can report whether the body needs rewinding.
618+
// This lets rewindBody avoid an error result when the request
619+
// does not have GetBody but the body hasn't been read at all yet.
620+
func setupRewindBody(req *Request) *Request {
621+
if req.Body == nil || req.Body == NoBody {
622+
return req
623+
}
624+
newReq := *req
625+
newReq.Body = &readTrackingBody{ReadCloser: req.Body}
626+
return &newReq
627+
}
628+
629+
// rewindBody returns a new request with the body rewound.
630+
// It returns req unmodified if the body does not need rewinding.
631+
// rewindBody takes care of closing req.Body when appropriate
632+
// (in all cases except when rewindBody returns req unmodified).
633+
func rewindBody(req *Request) (rewound *Request, err error) {
634+
if req.Body == nil || req.Body == NoBody || !req.Body.(*readTrackingBody).didRead {
635+
return req, nil // nothing to rewind
636+
}
637+
req.closeBody()
638+
if req.GetBody == nil {
639+
return nil, errCannotRewind
640+
}
641+
body, err := req.GetBody()
642+
if err != nil {
643+
return nil, err
644+
}
645+
newReq := *req
646+
newReq.Body = &readTrackingBody{ReadCloser: body}
647+
return &newReq, nil
648+
}
649+
602650
// shouldRetryRequest reports whether we should retry sending a failed
603651
// HTTP request on a new connection. The non-nil input error is the
604652
// error from roundTrip.

src/net/http/transport_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -6175,3 +6175,29 @@ func (timeoutProto) RoundTrip(req *Request) (*Response, error) {
61756175
return nil, errors.New("request was not canceled")
61766176
}
61776177
}
6178+
6179+
type roundTripFunc func(r *Request) (*Response, error)
6180+
6181+
func (f roundTripFunc) RoundTrip(r *Request) (*Response, error) { return f(r) }
6182+
6183+
// Issue 32441: body is not reset after ErrSkipAltProtocol
6184+
func TestIssue32441(t *testing.T) {
6185+
defer afterTest(t)
6186+
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
6187+
if n, _ := io.Copy(ioutil.Discard, r.Body); n == 0 {
6188+
t.Error("body length is zero")
6189+
}
6190+
}))
6191+
defer ts.Close()
6192+
c := ts.Client()
6193+
c.Transport.(*Transport).RegisterProtocol("http", roundTripFunc(func(r *Request) (*Response, error) {
6194+
// Draining body to trigger failure condition on actual request to server.
6195+
if n, _ := io.Copy(ioutil.Discard, r.Body); n == 0 {
6196+
t.Error("body length is zero during round trip")
6197+
}
6198+
return nil, ErrSkipAltProtocol
6199+
}))
6200+
if _, err := c.Post(ts.URL, "application/octet-stream", bytes.NewBufferString("data")); err != nil {
6201+
t.Error(err)
6202+
}
6203+
}

0 commit comments

Comments
 (0)