diff --git a/http2/errors.go b/http2/errors.go index 71f2c4631..c789fa338 100644 --- a/http2/errors.go +++ b/http2/errors.go @@ -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} } diff --git a/http2/transport.go b/http2/transport.go index dff4cc645..e896320ee 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -14,6 +14,7 @@ import ( "crypto/rand" "crypto/tls" "errors" + "expvar" "fmt" "io" "io/ioutil" @@ -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. @@ -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 @@ -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 @@ -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() @@ -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 @@ -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