Skip to content

Commit 945ddfd

Browse files
committed
http2: retry requests after receiving REFUSED STREAM
RoundTrip will retry a request if it receives REFUSED_STREAM. To guard against servers that use REFUSED_STREAM to encourage rate limiting, or servers that return REFUSED_STREAM deterministically for some requests, we retry after an exponential backoff and we cap the number of retries. The exponential backoff starts on the second retry, with a backoff sequence of 1s, 2s, 4s, etc, with 10% random jitter. The retry cap was set to 6, somewhat arbitrarily. Rationale: this is what Firefox does. Updates golang/go#20985 Change-Id: I4dcac4392ac4a3220d6d839f28bf943fe6b3fea7 Reviewed-on: https://go-review.googlesource.com/50471 Run-TryBot: Tom Bergan <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent f5079bd commit 945ddfd

File tree

2 files changed

+182
-39
lines changed

2 files changed

+182
-39
lines changed

http2/transport.go

Lines changed: 72 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"io/ioutil"
1919
"log"
2020
"math"
21+
mathrand "math/rand"
2122
"net"
2223
"net/http"
2324
"sort"
@@ -329,17 +330,33 @@ func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Res
329330
}
330331

331332
addr := authorityAddr(req.URL.Scheme, req.URL.Host)
332-
for {
333+
for retry := 0; ; retry++ {
333334
cc, err := t.connPool().GetClientConn(req, addr)
334335
if err != nil {
335336
t.vlogf("http2: Transport failed to get client conn for %s: %v", addr, err)
336337
return nil, err
337338
}
338339
traceGotConn(req, cc)
339340
res, err := cc.RoundTrip(req)
340-
if err != nil {
341-
if req, err = shouldRetryRequest(req, err); err == nil {
342-
continue
341+
if err != nil && retry <= 6 {
342+
afterBodyWrite := false
343+
if e, ok := err.(afterReqBodyWriteError); ok {
344+
err = e
345+
afterBodyWrite = true
346+
}
347+
if req, err = shouldRetryRequest(req, err, afterBodyWrite); err == nil {
348+
// After the first retry, do exponential backoff with 10% jitter.
349+
if retry == 0 {
350+
continue
351+
}
352+
backoff := float64(uint(1) << (uint(retry) - 1))
353+
backoff += backoff * (0.1 * mathrand.Float64())
354+
select {
355+
case <-time.After(time.Second * time.Duration(backoff)):
356+
continue
357+
case <-req.Context().Done():
358+
return nil, req.Context().Err()
359+
}
343360
}
344361
}
345362
if err != nil {
@@ -360,43 +377,60 @@ func (t *Transport) CloseIdleConnections() {
360377
}
361378

362379
var (
363-
errClientConnClosed = errors.New("http2: client conn is closed")
364-
errClientConnUnusable = errors.New("http2: client conn not usable")
365-
366-
errClientConnGotGoAway = errors.New("http2: Transport received Server's graceful shutdown GOAWAY")
367-
errClientConnGotGoAwayAfterSomeReqBody = errors.New("http2: Transport received Server's graceful shutdown GOAWAY; some request body already written")
380+
errClientConnClosed = errors.New("http2: client conn is closed")
381+
errClientConnUnusable = errors.New("http2: client conn not usable")
382+
errClientConnGotGoAway = errors.New("http2: Transport received Server's graceful shutdown GOAWAY")
368383
)
369384

385+
// afterReqBodyWriteError is a wrapper around errors returned by ClientConn.RoundTrip.
386+
// It is used to signal that err happened after part of Request.Body was sent to the server.
387+
type afterReqBodyWriteError struct {
388+
err error
389+
}
390+
391+
func (e afterReqBodyWriteError) Error() string {
392+
return e.err.Error() + "; some request body already written"
393+
}
394+
370395
// shouldRetryRequest is called by RoundTrip when a request fails to get
371396
// response headers. It is always called with a non-nil error.
372397
// It returns either a request to retry (either the same request, or a
373398
// modified clone), or an error if the request can't be replayed.
374-
func shouldRetryRequest(req *http.Request, err error) (*http.Request, error) {
375-
switch err {
376-
default:
399+
func shouldRetryRequest(req *http.Request, err error, afterBodyWrite bool) (*http.Request, error) {
400+
if !canRetryError(err) {
377401
return nil, err
378-
case errClientConnUnusable, errClientConnGotGoAway:
402+
}
403+
if !afterBodyWrite {
379404
return req, nil
380-
case errClientConnGotGoAwayAfterSomeReqBody:
381-
// If the Body is nil (or http.NoBody), it's safe to reuse
382-
// this request and its Body.
383-
if req.Body == nil || reqBodyIsNoBody(req.Body) {
384-
return req, nil
385-
}
386-
// Otherwise we depend on the Request having its GetBody
387-
// func defined.
388-
getBody := reqGetBody(req) // Go 1.8: getBody = req.GetBody
389-
if getBody == nil {
390-
return nil, errors.New("http2: Transport: peer server initiated graceful shutdown after some of Request.Body was written; define Request.GetBody to avoid this error")
391-
}
392-
body, err := getBody()
393-
if err != nil {
394-
return nil, err
395-
}
396-
newReq := *req
397-
newReq.Body = body
398-
return &newReq, nil
399405
}
406+
// If the Body is nil (or http.NoBody), it's safe to reuse
407+
// this request and its Body.
408+
if req.Body == nil || reqBodyIsNoBody(req.Body) {
409+
return req, nil
410+
}
411+
// Otherwise we depend on the Request having its GetBody
412+
// func defined.
413+
getBody := reqGetBody(req) // Go 1.8: getBody = req.GetBody
414+
if getBody == nil {
415+
return nil, fmt.Errorf("http2: Transport: cannot retry err [%v] after Request.Body was written; define Request.GetBody to avoid this error", err)
416+
}
417+
body, err := getBody()
418+
if err != nil {
419+
return nil, err
420+
}
421+
newReq := *req
422+
newReq.Body = body
423+
return &newReq, nil
424+
}
425+
426+
func canRetryError(err error) bool {
427+
if err == errClientConnUnusable || err == errClientConnGotGoAway {
428+
return true
429+
}
430+
if se, ok := err.(StreamError); ok {
431+
return se.Code == ErrCodeRefusedStream
432+
}
433+
return false
400434
}
401435

402436
func (t *Transport) dialClientConn(addr string, singleUse bool) (*ClientConn, error) {
@@ -816,14 +850,13 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
816850
cs.abortRequestBodyWrite(errStopReqBodyWrite)
817851
}
818852
if re.err != nil {
819-
if re.err == errClientConnGotGoAway {
820-
cc.mu.Lock()
821-
if cs.startedWrite {
822-
re.err = errClientConnGotGoAwayAfterSomeReqBody
823-
}
824-
cc.mu.Unlock()
825-
}
853+
cc.mu.Lock()
854+
afterBodyWrite := cs.startedWrite
855+
cc.mu.Unlock()
826856
cc.forgetStreamID(cs.ID)
857+
if afterBodyWrite {
858+
return nil, afterReqBodyWriteError{re.err}
859+
}
827860
return nil, re.err
828861
}
829862
res.Request = req

http2/transport_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2926,6 +2926,116 @@ func TestTransportRetryAfterGOAWAY(t *testing.T) {
29262926
}
29272927
}
29282928

2929+
func TestTransportRetryAfterRefusedStream(t *testing.T) {
2930+
clientDone := make(chan struct{})
2931+
ct := newClientTester(t)
2932+
ct.client = func() error {
2933+
defer ct.cc.(*net.TCPConn).CloseWrite()
2934+
defer close(clientDone)
2935+
req, _ := http.NewRequest("GET", "https://dummy.tld/", nil)
2936+
resp, err := ct.tr.RoundTrip(req)
2937+
if err != nil {
2938+
return fmt.Errorf("RoundTrip: %v", err)
2939+
}
2940+
resp.Body.Close()
2941+
if resp.StatusCode != 204 {
2942+
return fmt.Errorf("Status = %v; want 204", resp.StatusCode)
2943+
}
2944+
return nil
2945+
}
2946+
ct.server = func() error {
2947+
ct.greet()
2948+
var buf bytes.Buffer
2949+
enc := hpack.NewEncoder(&buf)
2950+
nreq := 0
2951+
2952+
for {
2953+
f, err := ct.fr.ReadFrame()
2954+
if err != nil {
2955+
select {
2956+
case <-clientDone:
2957+
// If the client's done, it
2958+
// will have reported any
2959+
// errors on its side.
2960+
return nil
2961+
default:
2962+
return err
2963+
}
2964+
}
2965+
switch f := f.(type) {
2966+
case *WindowUpdateFrame, *SettingsFrame:
2967+
case *HeadersFrame:
2968+
if !f.HeadersEnded() {
2969+
return fmt.Errorf("headers should have END_HEADERS be ended: %v", f)
2970+
}
2971+
nreq++
2972+
if nreq == 1 {
2973+
ct.fr.WriteRSTStream(f.StreamID, ErrCodeRefusedStream)
2974+
} else {
2975+
enc.WriteField(hpack.HeaderField{Name: ":status", Value: "204"})
2976+
ct.fr.WriteHeaders(HeadersFrameParam{
2977+
StreamID: f.StreamID,
2978+
EndHeaders: true,
2979+
EndStream: true,
2980+
BlockFragment: buf.Bytes(),
2981+
})
2982+
}
2983+
default:
2984+
return fmt.Errorf("Unexpected client frame %v", f)
2985+
}
2986+
}
2987+
}
2988+
ct.run()
2989+
}
2990+
2991+
func TestTransportRetryHasLimit(t *testing.T) {
2992+
// Skip in short mode because the total expected delay is 1s+2s+4s+8s+16s=29s.
2993+
if testing.Short() {
2994+
t.Skip("skipping long test in short mode")
2995+
}
2996+
clientDone := make(chan struct{})
2997+
ct := newClientTester(t)
2998+
ct.client = func() error {
2999+
defer ct.cc.(*net.TCPConn).CloseWrite()
3000+
defer close(clientDone)
3001+
req, _ := http.NewRequest("GET", "https://dummy.tld/", nil)
3002+
resp, err := ct.tr.RoundTrip(req)
3003+
if err == nil {
3004+
return fmt.Errorf("RoundTrip expected error, got response: %+v", resp)
3005+
}
3006+
t.Logf("expected error, got: %v", err)
3007+
return nil
3008+
}
3009+
ct.server = func() error {
3010+
ct.greet()
3011+
for {
3012+
f, err := ct.fr.ReadFrame()
3013+
if err != nil {
3014+
select {
3015+
case <-clientDone:
3016+
// If the client's done, it
3017+
// will have reported any
3018+
// errors on its side.
3019+
return nil
3020+
default:
3021+
return err
3022+
}
3023+
}
3024+
switch f := f.(type) {
3025+
case *WindowUpdateFrame, *SettingsFrame:
3026+
case *HeadersFrame:
3027+
if !f.HeadersEnded() {
3028+
return fmt.Errorf("headers should have END_HEADERS be ended: %v", f)
3029+
}
3030+
ct.fr.WriteRSTStream(f.StreamID, ErrCodeRefusedStream)
3031+
default:
3032+
return fmt.Errorf("Unexpected client frame %v", f)
3033+
}
3034+
}
3035+
}
3036+
ct.run()
3037+
}
3038+
29293039
func TestAuthorityAddr(t *testing.T) {
29303040
tests := []struct {
29313041
scheme, authority string

0 commit comments

Comments
 (0)