Skip to content

Commit

Permalink
http2: don't reuse Transport conns after seeing stream protocol errors
Browse files Browse the repository at this point in the history
  • Loading branch information
bradfitz committed Aug 11, 2021
1 parent 3fe7f64 commit a7fa738
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 3 deletions.
5 changes: 5 additions & 0 deletions http2/errors.go
Expand Up @@ -67,6 +67,11 @@ type StreamError struct {
Cause error // optional additional detail
}

// errFromPeer is a sentinel error value for StreamError.Cause to
// indicate that the StreamError was sent from the peer over the wire
// and wasn't locally generated in the Transport.
var errFromPeer = errors.New("received from peer")

func streamError(id uint32, code ErrCode) StreamError {
return StreamError{StreamID: id, Code: code}
}
Expand Down
27 changes: 24 additions & 3 deletions http2/transport.go
Expand Up @@ -14,6 +14,7 @@ import (
"crypto/rand"
"crypto/tls"
"errors"
"expvar"
"fmt"
"io"
"io/ioutil"
Expand All @@ -36,6 +37,8 @@ import (
"github.com/tailscale/net/idna"
)

var http2ClientGotProtoError = expvar.NewInt("http2client_got_protocol_error")

const (
// transportDefaultConnFlow is how many connection-level flow control
// tokens we give the server at start-up, past the default 64k.
Expand Down Expand Up @@ -244,6 +247,7 @@ type ClientConn struct {
cond *sync.Cond // hold mu; broadcast on flow/closed changes
flow flow // our conn-level flow control quota (cs.flow is per stream)
inflow flow // peer's conn-level flow control
doNotReuse bool
closing bool
closed bool
wantSettingsAck bool // we sent a SETTINGS frame and haven't heard back
Expand Down Expand Up @@ -558,6 +562,10 @@ func canRetryError(err error) bool {
return true
}
if se, ok := err.(StreamError); ok {
if se.Code == ErrCodeProtocol && se.Cause == errFromPeer {
// See golang/go#47635, golang/go#42777
return true
}
return se.Code == ErrCodeRefusedStream
}
return false
Expand Down Expand Up @@ -709,6 +717,13 @@ func (cc *ClientConn) healthCheck() {
}
}

// SetDoNotReuse marks cc as not reusable for future HTTP requests.
func (cc *ClientConn) SetDoNotReuse() {
cc.mu.Lock()
defer cc.mu.Unlock()
cc.doNotReuse = true
}

func (cc *ClientConn) setGoAway(f *GoAwayFrame) {
cc.mu.Lock()
defer cc.mu.Unlock()
Expand Down Expand Up @@ -771,6 +786,7 @@ func (cc *ClientConn) idleStateLocked() (st clientConnIdleState) {
}

st.canTakeNewRequest = cc.goAway == nil && !cc.closed && !cc.closing && maxConcurrentOkay &&
!cc.doNotReuse &&
int64(cc.nextStreamID)+2*int64(cc.pendingRequests) < math.MaxInt32 &&
!cc.tooIdleLocked()
st.freshConn = cc.nextStreamID == 1 && st.canTakeNewRequest
Expand Down Expand Up @@ -2420,10 +2436,15 @@ func (rl *clientConnReadLoop) processResetStream(f *RSTStreamFrame) error {
// which closes this, so there
// isn't a race.
default:
err := streamError(cs.ID, f.ErrCode)
cs.resetErr = err
serr := streamError(cs.ID, f.ErrCode)
if f.ErrCode == ErrCodeProtocol {
rl.cc.SetDoNotReuse()
http2ClientGotProtoError.Add(1)
serr.Cause = errFromPeer
}
cs.resetErr = serr
close(cs.peerReset)
cs.bufPipe.CloseWithError(err)
cs.bufPipe.CloseWithError(serr)
cs.cc.cond.Broadcast() // wake up checkResetOrDone via clientStream.awaitFlowControl
}
return nil
Expand Down

0 comments on commit a7fa738

Please sign in to comment.