Skip to content

Commit ec05fdc

Browse files
committed
http2: don't retry the first request on a connection on GOAWAY error
When a server sends a GOAWAY frame, it indicates the ID of the last stream it processed. We use this to mark any requests after that stream as being safe to retry on a new connection. Change this to not retry the first request on a connection if we get a GOAWAY with an error, even if the GOAWAY has a stream ID of 0 indicating that it didn't process that request. If we're getting an error as the first result on a new connection, then there's either something wrong with the server or something wrong with our request; either way, retrying isn't likely to be productive and may be unsafe. This matches the behavior of the HTTP/1 client, which also avoids retrying the first request on a new connection. For golang/go#66668 Fixes golang/go#60636 Change-Id: I90ea7cfce2974dd413f7cd8b78541678850376a5 Reviewed-on: https://go-review.googlesource.com/c/net/+/576895 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Jonathan Amsterdam <[email protected]>
1 parent b67a0f0 commit ec05fdc

File tree

2 files changed

+106
-3
lines changed

2 files changed

+106
-3
lines changed

http2/transport.go

+14-1
Original file line numberDiff line numberDiff line change
@@ -936,7 +936,20 @@ func (cc *ClientConn) setGoAway(f *GoAwayFrame) {
936936
}
937937
last := f.LastStreamID
938938
for streamID, cs := range cc.streams {
939-
if streamID > last {
939+
if streamID <= last {
940+
// The server's GOAWAY indicates that it received this stream.
941+
// It will either finish processing it, or close the connection
942+
// without doing so. Either way, leave the stream alone for now.
943+
continue
944+
}
945+
if streamID == 1 && cc.goAway.ErrCode != ErrCodeNo {
946+
// Don't retry the first stream on a connection if we get a non-NO error.
947+
// If the server is sending an error on a new connection,
948+
// retrying the request on a new one probably isn't going to work.
949+
cs.abortStreamLocked(fmt.Errorf("http2: Transport received GOAWAY from server ErrCode:%v", cc.goAway.ErrCode))
950+
} else {
951+
// Aborting the stream with errClentConnGotGoAway indicates that
952+
// the request should be retried on a new connection.
940953
cs.abortStreamLocked(errClientConnGotGoAway)
941954
}
942955
}

http2/transport_test.go

+92-2
Original file line numberDiff line numberDiff line change
@@ -3144,13 +3144,40 @@ func TestTransportPingWhenReadingPingDisabled(t *testing.T) {
31443144
}
31453145
}
31463146

3147-
func TestTransportRetryAfterGOAWAY(t *testing.T) {
3147+
func TestTransportRetryAfterGOAWAYNoRetry(t *testing.T) {
31483148
tt := newTestTransport(t)
31493149

31503150
req, _ := http.NewRequest("GET", "https://dummy.tld/", nil)
31513151
rt := tt.roundTrip(req)
31523152

3153-
// First attempt: Server sends a GOAWAY.
3153+
// First attempt: Server sends a GOAWAY with an error and
3154+
// a MaxStreamID less than the request ID.
3155+
// This probably indicates that there was something wrong with our request,
3156+
// so we don't retry it.
3157+
tc := tt.getConn()
3158+
tc.wantFrameType(FrameSettings)
3159+
tc.wantFrameType(FrameWindowUpdate)
3160+
tc.wantHeaders(wantHeader{
3161+
streamID: 1,
3162+
endStream: true,
3163+
})
3164+
tc.writeSettings()
3165+
tc.writeGoAway(0 /*max id*/, ErrCodeInternal, nil)
3166+
if rt.err() == nil {
3167+
t.Fatalf("after GOAWAY, RoundTrip is not done, want error")
3168+
}
3169+
}
3170+
3171+
func TestTransportRetryAfterGOAWAYRetry(t *testing.T) {
3172+
tt := newTestTransport(t)
3173+
3174+
req, _ := http.NewRequest("GET", "https://dummy.tld/", nil)
3175+
rt := tt.roundTrip(req)
3176+
3177+
// First attempt: Server sends a GOAWAY with ErrCodeNo and
3178+
// a MaxStreamID less than the request ID.
3179+
// We take the server at its word that nothing has really gone wrong,
3180+
// and retry the request.
31543181
tc := tt.getConn()
31553182
tc.wantFrameType(FrameSettings)
31563183
tc.wantFrameType(FrameWindowUpdate)
@@ -3185,6 +3212,69 @@ func TestTransportRetryAfterGOAWAY(t *testing.T) {
31853212
rt.wantStatus(200)
31863213
}
31873214

3215+
func TestTransportRetryAfterGOAWAYSecondRequest(t *testing.T) {
3216+
tt := newTestTransport(t)
3217+
3218+
// First request succeeds.
3219+
req, _ := http.NewRequest("GET", "https://dummy.tld/", nil)
3220+
rt1 := tt.roundTrip(req)
3221+
tc := tt.getConn()
3222+
tc.wantFrameType(FrameSettings)
3223+
tc.wantFrameType(FrameWindowUpdate)
3224+
tc.wantHeaders(wantHeader{
3225+
streamID: 1,
3226+
endStream: true,
3227+
})
3228+
tc.writeSettings()
3229+
tc.wantFrameType(FrameSettings) // Settings ACK
3230+
tc.writeHeaders(HeadersFrameParam{
3231+
StreamID: 1,
3232+
EndHeaders: true,
3233+
EndStream: true,
3234+
BlockFragment: tc.makeHeaderBlockFragment(
3235+
":status", "200",
3236+
),
3237+
})
3238+
rt1.wantStatus(200)
3239+
3240+
// Second request: Server sends a GOAWAY with
3241+
// a MaxStreamID less than the request ID.
3242+
// The server says it didn't see this request,
3243+
// so we retry it on a new connection.
3244+
req, _ = http.NewRequest("GET", "https://dummy.tld/", nil)
3245+
rt2 := tt.roundTrip(req)
3246+
3247+
// Second request, first attempt.
3248+
tc.wantHeaders(wantHeader{
3249+
streamID: 3,
3250+
endStream: true,
3251+
})
3252+
tc.writeSettings()
3253+
tc.writeGoAway(1 /*max id*/, ErrCodeProtocol, nil)
3254+
if rt2.done() {
3255+
t.Fatalf("after GOAWAY, RoundTrip is done; want it to be retrying")
3256+
}
3257+
3258+
// Second request, second attempt.
3259+
tc = tt.getConn()
3260+
tc.wantFrameType(FrameSettings)
3261+
tc.wantFrameType(FrameWindowUpdate)
3262+
tc.wantHeaders(wantHeader{
3263+
streamID: 1,
3264+
endStream: true,
3265+
})
3266+
tc.writeSettings()
3267+
tc.writeHeaders(HeadersFrameParam{
3268+
StreamID: 1,
3269+
EndHeaders: true,
3270+
EndStream: true,
3271+
BlockFragment: tc.makeHeaderBlockFragment(
3272+
":status", "200",
3273+
),
3274+
})
3275+
rt2.wantStatus(200)
3276+
}
3277+
31883278
func TestTransportRetryAfterRefusedStream(t *testing.T) {
31893279
tt := newTestTransport(t)
31903280

0 commit comments

Comments
 (0)