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.
This commit is contained in:
Alexander Neumann 2021-07-30 10:17:28 +02:00
parent 7d28006e2e
commit d8ea10db8c
1 changed files with 43 additions and 10 deletions

View File

@ -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