diff --git a/.changelog/11843.txt b/.changelog/11843.txt new file mode 100644 index 000000000..1d62d1a61 --- /dev/null +++ b/.changelog/11843.txt @@ -0,0 +1,3 @@ +```release-note:improvement +deps: use gorilla package for gzip http handler +``` diff --git a/api/api.go b/api/api.go index 8719cab2c..d8b90cc8d 100644 --- a/api/api.go +++ b/api/api.go @@ -741,35 +741,47 @@ func (c *Client) doRequest(r *request) (time.Duration, *http.Response, error) { if err != nil { return 0, nil, err } + start := time.Now() resp, err := c.httpClient.Do(req) diff := time.Since(start) // If the response is compressed, we swap the body's reader. - if resp != nil && resp.Header != nil { - var reader io.ReadCloser - switch resp.Header.Get("Content-Encoding") { - case "gzip": - greader, err := gzip.NewReader(resp.Body) - if err != nil { - return 0, nil, err - } - - // The gzip reader doesn't close the wrapped reader so we use - // multiCloser. - reader = &multiCloser{ - reader: greader, - inorderClose: []io.Closer{greader, resp.Body}, - } - default: - reader = resp.Body - } - resp.Body = reader + if zipErr := c.autoUnzip(resp); zipErr != nil { + return 0, nil, zipErr } return diff, resp, err } +// autoUnzip modifies resp in-place, wrapping the response body with a gzip +// reader if the Content-Encoding of the response is "gzip". +func (*Client) autoUnzip(resp *http.Response) error { + if resp == nil || resp.Header == nil { + return nil + } + + if resp.Header.Get("Content-Encoding") == "gzip" { + zReader, err := gzip.NewReader(resp.Body) + if err == io.EOF { + // zero length response, do not wrap + return nil + } else if err != nil { + // some other error (e.g. corrupt) + return err + } + + // The gzip reader does not close an underlying reader, so use a + // multiCloser to make sure response body does get closed. + resp.Body = &multiCloser{ + reader: zReader, + inorderClose: []io.Closer{zReader, resp.Body}, + } + } + + return nil +} + // rawQuery makes a GET request to the specified endpoint but returns just the // response body. func (c *Client) rawQuery(endpoint string, q *QueryOptions) (io.ReadCloser, error) { diff --git a/api/api_test.go b/api/api_test.go index a845f6410..3f83e716c 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -1,10 +1,13 @@ package api import ( + "bytes" + "compress/gzip" "context" "encoding/json" "errors" "fmt" + "io" "net/http" "net/http/httptest" "net/url" @@ -568,3 +571,49 @@ func TestClient_HeaderRaceCondition(t *testing.T) { require.Equal(2, <-c, "goroutine request should have two headers") require.Len(conf.Headers, 1, "config headers should not mutate") } + +func TestClient_autoUnzip(t *testing.T) { + var client *Client = nil + + try := func(resp *http.Response, exp error) { + err := client.autoUnzip(resp) + require.Equal(t, exp, err) + } + + // response object is nil + try(nil, nil) + + // response.Body is nil + try(new(http.Response), nil) + + // content-encoding is not gzip + try(&http.Response{ + Header: http.Header{"Content-Encoding": []string{"text"}}, + }, nil) + + // content-encoding is gzip but body is empty + try(&http.Response{ + Header: http.Header{"Content-Encoding": []string{"gzip"}}, + Body: io.NopCloser(bytes.NewBuffer([]byte{})), + }, nil) + + // content-encoding is gzip but body is invalid gzip + try(&http.Response{ + Header: http.Header{"Content-Encoding": []string{"gzip"}}, + Body: io.NopCloser(bytes.NewBuffer([]byte("not a zip"))), + }, errors.New("unexpected EOF")) + + // sample gzip payload + var b bytes.Buffer + w := gzip.NewWriter(&b) + _, err := w.Write([]byte("hello world")) + require.NoError(t, err) + err = w.Close() + require.NoError(t, err) + + // content-encoding is gzip and body is gzip data + try(&http.Response{ + Header: http.Header{"Content-Encoding": []string{"gzip"}}, + Body: io.NopCloser(&b), + }, nil) +}