fserrors: make http2 "server sent GOAWAY" a retriable error - fixes #9664

When an HTTP/2 server retires a connection with GOAWAY after it has
already sent successful response headers, Go's http2 transport fails the
read of the response body with

    http2: server sent GOAWAY and closed the connection; LastStreamID=..., ErrCode=NO_ERROR, debug=""

This was not recognised as a retriable networking error, so a transient
connection retirement aborted the whole command instead of consuming a
low level retry. It was reported against a large S3 check, where an
interrupted ListObjectsV2 page made rclone report destination objects as
missing and exit unsuccessfully.

The concrete error type is unexported by net/http, so match on the
message as we already do for the other http2 transport errors.
This commit is contained in:
phatlc
2026-07-29 17:35:11 +01:00
committed by Nick Craig-Wood
parent 0f49ceab75
commit ab93058560
2 changed files with 5 additions and 0 deletions
+1
View File
@@ -386,6 +386,7 @@ var retriableErrorStrings = []string{
"server closed idle connection", // net/http/transport.go
"bad record MAC", // crypto/tls/alert.go
"stream error:", // net/http/h2_bundle.go
"http2: server sent GOAWAY", // net/http/h2_bundle.go
"tls: use of closed connection", // crypto/tls/conn.go
}
+4
View File
@@ -38,6 +38,8 @@ func wrap(err error, message string) error {
var errUseOfClosedNetworkConnection = errors.New("use of closed network connection")
var errHTTP2GoAway = errors.New(`http2: server sent GOAWAY and closed the connection; LastStreamID=19999, ErrCode=NO_ERROR, debug=""`)
type myError1 struct {
Err error
}
@@ -135,6 +137,8 @@ func TestShouldRetry(t *testing.T) {
{&url.Error{Op: "post", URL: "/", Err: io.EOF}, true},
{&url.Error{Op: "post", URL: "/", Err: errUseOfClosedNetworkConnection}, true},
{&url.Error{Op: "post", URL: "/", Err: fmt.Errorf("net/http: HTTP/1.x transport connection broken: %v", fmt.Errorf("http: ContentLength=%d with Body length %d", 100663336, 99590598))}, true},
{errHTTP2GoAway, true},
{fmt.Errorf("operation error S3: ListObjectsV2, deserialization failed, failed to decode response body: %w", errHTTP2GoAway), true},
} {
got := ShouldRetry(test.err)
assert.Equal(t, test.want, got, fmt.Sprintf("test #%d: %v", i, test.err))