From d8ea10db8c020ad17a68dfe27f301f2b8b0f6073 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Fri, 30 Jul 2021 10:17:28 +0200 Subject: [PATCH] rest: Rework handling HTTP2 zero-length replies bug Add comment that the check is based on the stdlib HTTP2 client. Refactor the checks into a function. Return an error if the value in the Content-Length header cannot be parsed. --- internal/backend/rest/rest.go | 53 ++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/internal/backend/rest/rest.go b/internal/backend/rest/rest.go index 78cb6b10b..55732e871 100644 --- a/internal/backend/rest/rest.go +++ b/internal/backend/rest/rest.go @@ -7,6 +7,7 @@ import ( "io" "io/ioutil" "net/http" + "net/textproto" "net/url" "path" "strconv" @@ -198,6 +199,44 @@ func (b *Backend) Load(ctx context.Context, h restic.Handle, length int, offset return err } +// checkContentLength returns an error if the server returned a value in the +// Content-Length header in an HTTP2 connection, but closed the connection +// before any data was sent. +// +// This is a workaround for https://github.com/golang/go/issues/46071 +// +// See also https://forum.restic.net/t/http2-stream-closed-connection-reset-context-canceled/3743/10 +func checkContentLength(resp *http.Response) error { + // the following code is based on + // https://github.com/golang/go/blob/b7a85e0003cedb1b48a1fd3ae5b746ec6330102e/src/net/http/h2_bundle.go#L8646 + + if resp.ContentLength != 0 { + return nil + } + + if resp.ProtoMajor != 2 && resp.ProtoMinor != 0 { + return nil + } + + if len(resp.Header[textproto.CanonicalMIMEHeaderKey("Content-Length")]) != 1 { + return nil + } + + // make sure that if the server returned a content length and we can + // parse it, it is really zero, otherwise return an error + contentLength := resp.Header.Get("Content-Length") + cl, err := strconv.ParseUint(contentLength, 10, 63) + if err != nil { + return fmt.Errorf("unable to parse Content-Length %q: %w", contentLength, err) + } + + if cl != 0 { + return errors.Errorf("unexpected EOF: got 0 instead of %v bytes", cl) + } + + return nil +} + func (b *Backend) openReader(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) { debug.Log("Load %v, length %v, offset %v", h, length, offset) if err := h.Valid(); err != nil { @@ -249,16 +288,10 @@ func (b *Backend) openReader(ctx context.Context, h restic.Handle, length int, o // workaround https://github.com/golang/go/issues/46071 // see also https://forum.restic.net/t/http2-stream-closed-connection-reset-context-canceled/3743/10 - if resp.ContentLength == 0 && resp.ProtoMajor == 2 && resp.ProtoMinor == 0 { - if clens := resp.Header["Content-Length"]; len(clens) == 1 { - if cl, err := strconv.ParseUint(clens[0], 10, 63); err == nil { - resp.ContentLength = int64(cl) - } - if resp.ContentLength != 0 { - _ = resp.Body.Close() - return nil, errors.Errorf("unexpected EOF got 0 instead of %v bytes", resp.ContentLength) - } - } + err = checkContentLength(resp) + if err != nil { + _ = resp.Body.Close() + return nil, err } return resp.Body, nil