From ab930585609009a508fa3f1e0499e344f3b49840 Mon Sep 17 00:00:00 2001 From: phatlc Date: Wed, 29 Jul 2026 01:26:40 +0700 Subject: [PATCH] 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. --- fs/fserrors/error.go | 1 + fs/fserrors/error_test.go | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/fs/fserrors/error.go b/fs/fserrors/error.go index e65bd5be5..413a5a1f9 100644 --- a/fs/fserrors/error.go +++ b/fs/fserrors/error.go @@ -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 } diff --git a/fs/fserrors/error_test.go b/fs/fserrors/error_test.go index 21bdaf5cb..b35806fa9 100644 --- a/fs/fserrors/error_test.go +++ b/fs/fserrors/error_test.go @@ -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))