Skip to content

Commit a7fa738

Browse files
committed
http2: don't reuse Transport conns after seeing stream protocol errors
Updates tailscale/corp#2363 Updates golang/go#47635 Updates golang/go#42777
1 parent 3fe7f64 commit a7fa738

File tree

2 files changed

+29
-3
lines changed

2 files changed

+29
-3
lines changed

http2/errors.go

+5
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ type StreamError struct {
6767
Cause error // optional additional detail
6868
}
6969

70+
// errFromPeer is a sentinel error value for StreamError.Cause to
71+
// indicate that the StreamError was sent from the peer over the wire
72+
// and wasn't locally generated in the Transport.
73+
var errFromPeer = errors.New("received from peer")
74+
7075
func streamError(id uint32, code ErrCode) StreamError {
7176
return StreamError{StreamID: id, Code: code}
7277
}

http2/transport.go

+24-3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"crypto/rand"
1515
"crypto/tls"
1616
"errors"
17+
"expvar"
1718
"fmt"
1819
"io"
1920
"io/ioutil"
@@ -36,6 +37,8 @@ import (
3637
"github.com/tailscale/net/idna"
3738
)
3839

40+
var http2ClientGotProtoError = expvar.NewInt("http2client_got_protocol_error")
41+
3942
const (
4043
// transportDefaultConnFlow is how many connection-level flow control
4144
// tokens we give the server at start-up, past the default 64k.
@@ -244,6 +247,7 @@ type ClientConn struct {
244247
cond *sync.Cond // hold mu; broadcast on flow/closed changes
245248
flow flow // our conn-level flow control quota (cs.flow is per stream)
246249
inflow flow // peer's conn-level flow control
250+
doNotReuse bool
247251
closing bool
248252
closed bool
249253
wantSettingsAck bool // we sent a SETTINGS frame and haven't heard back
@@ -558,6 +562,10 @@ func canRetryError(err error) bool {
558562
return true
559563
}
560564
if se, ok := err.(StreamError); ok {
565+
if se.Code == ErrCodeProtocol && se.Cause == errFromPeer {
566+
// See golang/go#47635, golang/go#42777
567+
return true
568+
}
561569
return se.Code == ErrCodeRefusedStream
562570
}
563571
return false
@@ -709,6 +717,13 @@ func (cc *ClientConn) healthCheck() {
709717
}
710718
}
711719

720+
// SetDoNotReuse marks cc as not reusable for future HTTP requests.
721+
func (cc *ClientConn) SetDoNotReuse() {
722+
cc.mu.Lock()
723+
defer cc.mu.Unlock()
724+
cc.doNotReuse = true
725+
}
726+
712727
func (cc *ClientConn) setGoAway(f *GoAwayFrame) {
713728
cc.mu.Lock()
714729
defer cc.mu.Unlock()
@@ -771,6 +786,7 @@ func (cc *ClientConn) idleStateLocked() (st clientConnIdleState) {
771786
}
772787

773788
st.canTakeNewRequest = cc.goAway == nil && !cc.closed && !cc.closing && maxConcurrentOkay &&
789+
!cc.doNotReuse &&
774790
int64(cc.nextStreamID)+2*int64(cc.pendingRequests) < math.MaxInt32 &&
775791
!cc.tooIdleLocked()
776792
st.freshConn = cc.nextStreamID == 1 && st.canTakeNewRequest
@@ -2420,10 +2436,15 @@ func (rl *clientConnReadLoop) processResetStream(f *RSTStreamFrame) error {
24202436
// which closes this, so there
24212437
// isn't a race.
24222438
default:
2423-
err := streamError(cs.ID, f.ErrCode)
2424-
cs.resetErr = err
2439+
serr := streamError(cs.ID, f.ErrCode)
2440+
if f.ErrCode == ErrCodeProtocol {
2441+
rl.cc.SetDoNotReuse()
2442+
http2ClientGotProtoError.Add(1)
2443+
serr.Cause = errFromPeer
2444+
}
2445+
cs.resetErr = serr
24252446
close(cs.peerReset)
2426-
cs.bufPipe.CloseWithError(err)
2447+
cs.bufPipe.CloseWithError(serr)
24272448
cs.cc.cond.Broadcast() // wake up checkResetOrDone via clientStream.awaitFlowControl
24282449
}
24292450
return nil

0 commit comments

Comments
 (0)