github.com/grafana/pyroscope@v1.18.0/pkg/util/gziphandler/gzip_test.go (about)

     1  // SPDX-License-Identifier: AGPL-3.0-only
     2  // Provenance-includes-location: https://github.com/nytimes/gziphandler/blob/2f8bb1d30d9d69c8e0c3714da5a9917125a87769/gzip_test.go
     3  // Provenance-includes-license: Apache-2.0
     4  // Provenance-includes-copyright: Copyright 2016-2017 The New York Times Company.
     5  package gziphandler
     6  
     7  import (
     8  	"bytes"
     9  	"compress/gzip"
    10  	"context"
    11  	"fmt"
    12  	"io"
    13  	"net"
    14  	"net/http"
    15  	"net/http/httptest"
    16  	"net/url"
    17  	"os"
    18  	"strconv"
    19  	"strings"
    20  	"testing"
    21  
    22  	"github.com/stretchr/testify/assert"
    23  	"google.golang.org/grpc/test/bufconn"
    24  )
    25  
    26  const (
    27  	smallTestBody = "aaabbcaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbc"
    28  	testBody      = "aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc"
    29  )
    30  
    31  func TestParseEncodings(t *testing.T) {
    32  	examples := map[string]codings{
    33  
    34  		// Examples from RFC 2616
    35  		"compress, gzip":                     {"compress": 1.0, "gzip": 1.0},
    36  		"":                                   {},
    37  		"*":                                  {"*": 1.0},
    38  		"compress;q=0.5, gzip;q=1.0":         {"compress": 0.5, "gzip": 1.0},
    39  		"gzip;q=1.0, identity; q=0.5, *;q=0": {"gzip": 1.0, "identity": 0.5, "*": 0.0},
    40  		"gzip;q=1.0, identity;q=0":           {"gzip": 1.0, "identity": 0.0},
    41  
    42  		// More random stuff
    43  		"AAA;q=1":     {"aaa": 1.0},
    44  		"BBB ; q = 2": {"bbb": 1.0},
    45  	}
    46  
    47  	for eg, exp := range examples {
    48  		act, _ := parseEncodings(eg)
    49  		assert.Equal(t, exp, act)
    50  	}
    51  }
    52  
    53  func TestRequestAcceptance(t *testing.T) {
    54  	type ret struct {
    55  		acceptsGzip     bool
    56  		rejectsIdentity bool
    57  	}
    58  
    59  	for header, expected := range map[string]ret{
    60  		"gzip":                            {true, false},
    61  		"gzip;q=1":                        {true, false},
    62  		"gzip;q=1, identity;q=0":          {true, true},
    63  		"gzip;q=1, identity;q=0, *;q=0.5": {true, true},
    64  		"foo;q=1, gzip;q=0.5, *;q=0":      {true, true},
    65  		"identity;q=0":                    {false, true},
    66  		"identity;q=0, *;q=0.5":           {true, true},
    67  	} {
    68  		t.Run(header, func(t *testing.T) {
    69  			req, err := http.NewRequest(http.MethodGet, "http://localhost", nil)
    70  			req.Header.Set(acceptEncoding, header)
    71  			assert.NoError(t, err)
    72  
    73  			acceptsGzip, acceptsIdentity := requestAcceptance(req)
    74  			assert.Equal(t, expected.acceptsGzip, acceptsGzip, "acceptsGzip differs")
    75  			assert.Equal(t, expected.rejectsIdentity, acceptsIdentity, "rejectsIdentity differs")
    76  		})
    77  	}
    78  }
    79  
    80  func TestGzipHandler(t *testing.T) {
    81  	// This just exists to provide something for GzipHandler to wrap.
    82  	handler := newTestHandler(testBody)
    83  
    84  	// requests without accept-encoding are passed along as-is
    85  
    86  	req1, _ := http.NewRequest("GET", "/whatever", nil)
    87  	resp1 := httptest.NewRecorder()
    88  	handler.ServeHTTP(resp1, req1)
    89  	res1 := resp1.Result()
    90  
    91  	assert.Equal(t, 200, res1.StatusCode)
    92  	assert.Equal(t, "", res1.Header.Get("Content-Encoding"))
    93  	assert.Equal(t, "Accept-Encoding", res1.Header.Get("Vary"))
    94  	assert.Equal(t, testBody, resp1.Body.String())
    95  
    96  	// but requests with accept-encoding:gzip are compressed if possible
    97  
    98  	req2, _ := http.NewRequest("GET", "/whatever", nil)
    99  	req2.Header.Set("Accept-Encoding", "gzip")
   100  	resp2 := httptest.NewRecorder()
   101  	handler.ServeHTTP(resp2, req2)
   102  	res2 := resp2.Result()
   103  
   104  	assert.Equal(t, 200, res2.StatusCode)
   105  	assert.Equal(t, "gzip", res2.Header.Get("Content-Encoding"))
   106  	assert.Equal(t, "Accept-Encoding", res2.Header.Get("Vary"))
   107  	assert.Equal(t, gzipStrLevel(testBody, gzip.DefaultCompression), resp2.Body.Bytes())
   108  
   109  	// content-type header is correctly set based on uncompressed body
   110  
   111  	req3, _ := http.NewRequest("GET", "/whatever", nil)
   112  	req3.Header.Set("Accept-Encoding", "gzip")
   113  	res3 := httptest.NewRecorder()
   114  	handler.ServeHTTP(res3, req3)
   115  
   116  	assert.Equal(t, http.DetectContentType([]byte(testBody)), res3.Header().Get("Content-Type"))
   117  }
   118  
   119  func TestGzipHandlerSmallBodyNoCompression(t *testing.T) {
   120  	handler := newTestHandler(smallTestBody)
   121  
   122  	req, _ := http.NewRequest("GET", "/whatever", nil)
   123  	req.Header.Set("Accept-Encoding", "gzip")
   124  	resp := httptest.NewRecorder()
   125  	handler.ServeHTTP(resp, req)
   126  	res := resp.Result()
   127  
   128  	// with less than 1400 bytes the response should not be gzipped
   129  
   130  	assert.Equal(t, 200, res.StatusCode)
   131  	assert.Equal(t, "", res.Header.Get("Content-Encoding"))
   132  	assert.Equal(t, "Accept-Encoding", res.Header.Get("Vary"))
   133  	assert.Equal(t, smallTestBody, resp.Body.String())
   134  }
   135  
   136  func TestGzipHandlerSmallButDoesNotAcceptIdentity(t *testing.T) {
   137  	handler := newTestHandler(smallTestBody)
   138  
   139  	req, _ := http.NewRequest("GET", "/whatever", nil)
   140  	req.Header.Set("Accept-Encoding", "gzip;q=1, identity;q=0")
   141  	resp := httptest.NewRecorder()
   142  	handler.ServeHTTP(resp, req)
   143  	res := resp.Result()
   144  
   145  	// We explicitly stated that we will reject an uncompressed response.
   146  
   147  	assert.Equal(t, 200, res.StatusCode)
   148  	assert.Equal(t, "gzip", res.Header.Get(contentEncoding))
   149  }
   150  
   151  func TestGzipHandlerAlreadyCompressed(t *testing.T) {
   152  	handler := newTestHandler(testBody)
   153  
   154  	req, _ := http.NewRequest("GET", "/gzipped", nil)
   155  	req.Header.Set("Accept-Encoding", "gzip")
   156  	res := httptest.NewRecorder()
   157  	handler.ServeHTTP(res, req)
   158  
   159  	assert.Equal(t, testBody, res.Body.String())
   160  }
   161  
   162  func TestNewGzipLevelHandler(t *testing.T) {
   163  	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   164  		w.WriteHeader(http.StatusOK)
   165  		_, _ = io.WriteString(w, testBody)
   166  	})
   167  
   168  	for lvl := gzip.BestSpeed; lvl <= gzip.BestCompression; lvl++ {
   169  		wrapper, err := NewGzipLevelHandler(lvl)
   170  		if !assert.Nil(t, err, "NewGzipLevleHandler returned error for level:", lvl) {
   171  			continue
   172  		}
   173  
   174  		req, _ := http.NewRequest("GET", "/whatever", nil)
   175  		req.Header.Set("Accept-Encoding", "gzip")
   176  		resp := httptest.NewRecorder()
   177  		wrapper(handler).ServeHTTP(resp, req)
   178  		res := resp.Result()
   179  
   180  		assert.Equal(t, 200, res.StatusCode)
   181  		assert.Equal(t, "gzip", res.Header.Get("Content-Encoding"))
   182  		assert.Equal(t, "Accept-Encoding", res.Header.Get("Vary"))
   183  		assert.Equal(t, gzipStrLevel(testBody, lvl), resp.Body.Bytes())
   184  	}
   185  }
   186  
   187  func TestNewGzipLevelHandlerReturnsErrorForInvalidLevels(t *testing.T) {
   188  	var err error
   189  	_, err = NewGzipLevelHandler(-42)
   190  	assert.NotNil(t, err)
   191  
   192  	_, err = NewGzipLevelHandler(42)
   193  	assert.NotNil(t, err)
   194  }
   195  
   196  func TestMustNewGzipLevelHandlerWillPanic(t *testing.T) {
   197  	defer func() {
   198  		if r := recover(); r == nil {
   199  			t.Error("panic was not called")
   200  		}
   201  	}()
   202  
   203  	_ = MustNewGzipLevelHandler(-42)
   204  }
   205  
   206  func TestGzipHandlerNoBody(t *testing.T) {
   207  	tests := []struct {
   208  		statusCode      int
   209  		contentEncoding string
   210  		emptyBody       bool
   211  		body            []byte
   212  	}{
   213  		// Body must be empty.
   214  		{http.StatusNoContent, "", true, nil},
   215  		{http.StatusNotModified, "", true, nil},
   216  		// Body is going to get gzip'd no matter what.
   217  		{http.StatusOK, "", true, []byte{}},
   218  		{http.StatusOK, "gzip", false, []byte(testBody)},
   219  	}
   220  
   221  	for num, test := range tests {
   222  		handler := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   223  			w.WriteHeader(test.statusCode)
   224  			if test.body != nil {
   225  				_, _ = w.Write(test.body)
   226  			}
   227  		}))
   228  
   229  		rec := httptest.NewRecorder()
   230  		// TODO: in Go1.7 httptest.NewRequest was introduced this should be used
   231  		// once 1.6 is not longer supported.
   232  		req := &http.Request{
   233  			Method:     "GET",
   234  			URL:        &url.URL{Path: "/"},
   235  			Proto:      "HTTP/1.1",
   236  			ProtoMinor: 1,
   237  			RemoteAddr: "192.0.2.1:1234",
   238  			Header:     make(http.Header),
   239  		}
   240  		req.Header.Set("Accept-Encoding", "gzip")
   241  		handler.ServeHTTP(rec, req)
   242  
   243  		body, err := io.ReadAll(rec.Body)
   244  		if err != nil {
   245  			t.Fatalf("Unexpected error reading response body: %v", err)
   246  		}
   247  
   248  		header := rec.Header()
   249  		assert.Equal(t, test.contentEncoding, header.Get("Content-Encoding"), fmt.Sprintf("for test iteration %d", num))
   250  		assert.Equal(t, "Accept-Encoding", header.Get("Vary"), fmt.Sprintf("for test iteration %d", num))
   251  		if test.emptyBody {
   252  			assert.Empty(t, body, fmt.Sprintf("for test iteration %d", num))
   253  		} else {
   254  			assert.NotEmpty(t, body, fmt.Sprintf("for test iteration %d", num))
   255  			assert.NotEqual(t, test.body, body, fmt.Sprintf("for test iteration %d", num))
   256  		}
   257  	}
   258  }
   259  
   260  func TestGzipHandlerContentLength(t *testing.T) {
   261  	testBodyBytes := []byte(testBody)
   262  	tests := []struct {
   263  		bodyLen   int
   264  		bodies    [][]byte
   265  		emptyBody bool
   266  	}{
   267  		{len(testBody), [][]byte{testBodyBytes}, false},
   268  		// each of these writes is less than the DefaultMinSize
   269  		{len(testBody), [][]byte{testBodyBytes[:200], testBodyBytes[200:]}, false},
   270  		// without a defined Content-Length it should still gzip
   271  		{0, [][]byte{testBodyBytes[:200], testBodyBytes[200:]}, false},
   272  		// simulate a HEAD request with an empty write (to populate headers)
   273  		{len(testBody), [][]byte{nil}, true},
   274  	}
   275  
   276  	// httptest.NewRecorder doesn't give you access to the Content-Length
   277  	// header so instead, we create an in-memory network connection to a server.
   278  	ln := bufconn.Listen(256 << 10)
   279  	defer ln.Close()
   280  	srv := &http.Server{
   281  		Handler: nil,
   282  	}
   283  	go func() { _ = srv.Serve(ln) }()
   284  
   285  	for num, test := range tests {
   286  		srv.Handler = GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   287  			if test.bodyLen > 0 {
   288  				w.Header().Set("Content-Length", strconv.Itoa(test.bodyLen))
   289  			}
   290  			for _, b := range test.bodies {
   291  				_, _ = w.Write(b)
   292  			}
   293  		}))
   294  		req := &http.Request{
   295  			Method: "GET",
   296  			URL:    &url.URL{Path: "/", Scheme: "http", Host: ln.Addr().String()},
   297  			Header: make(http.Header),
   298  			Close:  true,
   299  		}
   300  		req.Header.Set("Accept-Encoding", "gzip")
   301  
   302  		client := http.Client{
   303  			Transport: &http.Transport{
   304  				DialContext: func(context.Context, string, string) (net.Conn, error) { return ln.Dial() },
   305  			},
   306  		}
   307  		res, err := client.Do(req)
   308  		if err != nil {
   309  			t.Fatalf("Unexpected error making http request in test iteration %d: %v", num, err)
   310  		}
   311  		defer res.Body.Close()
   312  
   313  		body, err := io.ReadAll(res.Body)
   314  		if err != nil {
   315  			t.Fatalf("Unexpected error reading response body in test iteration %d: %v", num, err)
   316  		}
   317  
   318  		l, err := strconv.Atoi(res.Header.Get("Content-Length"))
   319  		if err != nil {
   320  			t.Fatalf("Unexpected error parsing Content-Length in test iteration %d: %v", num, err)
   321  		}
   322  		if test.emptyBody {
   323  			assert.Empty(t, body, fmt.Sprintf("for test iteration %d", num))
   324  			assert.Equal(t, 0, l, fmt.Sprintf("for test iteration %d", num))
   325  		} else {
   326  			assert.Len(t, body, l, fmt.Sprintf("for test iteration %d", num))
   327  		}
   328  		assert.Equal(t, "gzip", res.Header.Get("Content-Encoding"), fmt.Sprintf("for test iteration %d", num))
   329  		assert.NotEqual(t, test.bodyLen, l, fmt.Sprintf("for test iteration %d", num))
   330  	}
   331  }
   332  
   333  func TestGzipHandlerMinSizeMustBePositive(t *testing.T) {
   334  	_, err := NewGzipLevelAndMinSize(gzip.DefaultCompression, -1)
   335  	assert.Error(t, err)
   336  }
   337  
   338  func TestGzipHandlerMinSize(t *testing.T) {
   339  	responseLength := 0
   340  	b := []byte{'x'}
   341  
   342  	wrapper, _ := NewGzipLevelAndMinSize(gzip.DefaultCompression, 128)
   343  	handler := wrapper(http.HandlerFunc(
   344  		func(w http.ResponseWriter, r *http.Request) {
   345  			// Write responses one byte at a time to ensure that the flush
   346  			// mechanism, if used, is working properly.
   347  			for i := 0; i < responseLength; i++ {
   348  				n, err := w.Write(b)
   349  				assert.Equal(t, 1, n)
   350  				assert.Nil(t, err)
   351  			}
   352  		},
   353  	))
   354  
   355  	r, _ := http.NewRequest("GET", "/whatever", &bytes.Buffer{})
   356  	r.Header.Add("Accept-Encoding", "gzip")
   357  
   358  	// Short response is not compressed
   359  	responseLength = 127
   360  	w := httptest.NewRecorder()
   361  	handler.ServeHTTP(w, r)
   362  	if w.Result().Header.Get(contentEncoding) == "gzip" {
   363  		t.Error("Expected uncompressed response, got compressed")
   364  	}
   365  
   366  	// Long response is not compressed
   367  	responseLength = 128
   368  	w = httptest.NewRecorder()
   369  	handler.ServeHTTP(w, r)
   370  	if w.Result().Header.Get(contentEncoding) != "gzip" {
   371  		t.Error("Expected compressed response, got uncompressed")
   372  	}
   373  }
   374  
   375  func TestGzipDoubleClose(t *testing.T) {
   376  	// reset the pool for the default compression so we can make sure duplicates
   377  	// aren't added back by double close
   378  	addLevelPool(gzip.DefaultCompression)
   379  
   380  	handler := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   381  		// call close here and it'll get called again interally by
   382  		// NewGzipLevelHandler's handler defer
   383  		_, _ = w.Write([]byte("test"))
   384  		w.(io.Closer).Close()
   385  	}))
   386  
   387  	r := httptest.NewRequest("GET", "/", nil)
   388  	r.Header.Set("Accept-Encoding", "gzip")
   389  	w := httptest.NewRecorder()
   390  	handler.ServeHTTP(w, r)
   391  
   392  	// the second close shouldn't have added the same writer
   393  	// so we pull out 2 writers from the pool and make sure they're different
   394  	w1 := gzipWriterPools[poolIndex(gzip.DefaultCompression)].Get()
   395  	w2 := gzipWriterPools[poolIndex(gzip.DefaultCompression)].Get()
   396  	// assert.NotEqual looks at the value and not the address, so we use regular ==
   397  	assert.False(t, w1 == w2)
   398  }
   399  
   400  type panicOnSecondWriteHeaderWriter struct {
   401  	http.ResponseWriter
   402  	headerWritten bool
   403  }
   404  
   405  func (w *panicOnSecondWriteHeaderWriter) WriteHeader(s int) {
   406  	if w.headerWritten {
   407  		panic("header already written")
   408  	}
   409  	w.headerWritten = true
   410  	w.ResponseWriter.WriteHeader(s)
   411  }
   412  
   413  func TestGzipHandlerDoubleWriteHeader(t *testing.T) {
   414  	handler := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   415  		w.Header().Set("Content-Length", "15000")
   416  		// Specifically write the header here
   417  		w.WriteHeader(304)
   418  		// Ensure that after a Write the header isn't triggered again on close
   419  		_, _ = w.Write(nil)
   420  	}))
   421  	wrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   422  		w = &panicOnSecondWriteHeaderWriter{
   423  			ResponseWriter: w,
   424  		}
   425  		handler.ServeHTTP(w, r)
   426  	})
   427  
   428  	rec := httptest.NewRecorder()
   429  	// TODO: in Go1.7 httptest.NewRequest was introduced this should be used
   430  	// once 1.6 is not longer supported.
   431  	req := &http.Request{
   432  		Method:     "GET",
   433  		URL:        &url.URL{Path: "/"},
   434  		Proto:      "HTTP/1.1",
   435  		ProtoMinor: 1,
   436  		RemoteAddr: "192.0.2.1:1234",
   437  		Header:     make(http.Header),
   438  	}
   439  	req.Header.Set("Accept-Encoding", "gzip")
   440  	wrapper.ServeHTTP(rec, req)
   441  	body, err := io.ReadAll(rec.Body)
   442  	if err != nil {
   443  		t.Fatalf("Unexpected error reading response body: %v", err)
   444  	}
   445  	assert.Empty(t, body)
   446  	header := rec.Header()
   447  	assert.Equal(t, "gzip", header.Get("Content-Encoding"))
   448  	assert.Equal(t, "Accept-Encoding", header.Get("Vary"))
   449  	assert.Equal(t, 304, rec.Code)
   450  }
   451  
   452  func TestStatusCodes(t *testing.T) {
   453  	handler := GzipHandler(http.NotFoundHandler())
   454  	r := httptest.NewRequest("GET", "/", nil)
   455  	r.Header.Set("Accept-Encoding", "gzip")
   456  	w := httptest.NewRecorder()
   457  	handler.ServeHTTP(w, r)
   458  
   459  	result := w.Result()
   460  	if result.StatusCode != 404 {
   461  		t.Errorf("StatusCode should have been 404 but was %d", result.StatusCode)
   462  	}
   463  }
   464  
   465  func TestFlushBeforeWrite(t *testing.T) {
   466  	b := []byte(testBody)
   467  	handler := GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
   468  		rw.WriteHeader(http.StatusNotFound)
   469  		rw.(http.Flusher).Flush()
   470  		_, _ = rw.Write(b)
   471  	}))
   472  	r := httptest.NewRequest(http.MethodGet, "/", nil)
   473  	r.Header.Set("Accept-Encoding", "gzip")
   474  	w := httptest.NewRecorder()
   475  	handler.ServeHTTP(w, r)
   476  
   477  	res := w.Result()
   478  	assert.Equal(t, http.StatusNotFound, res.StatusCode)
   479  	assert.Equal(t, "gzip", res.Header.Get("Content-Encoding"))
   480  	assert.NotEqual(t, b, w.Body.Bytes())
   481  }
   482  
   483  func TestImplementFlusher(t *testing.T) {
   484  	request := httptest.NewRequest(http.MethodGet, "/", nil)
   485  	request.Header.Set(acceptEncoding, "gzip")
   486  	GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
   487  		_, okFlusher := rw.(http.Flusher)
   488  		assert.True(t, okFlusher, "response writer must implement http.Flusher")
   489  	})).ServeHTTP(httptest.NewRecorder(), request)
   490  }
   491  
   492  func TestIgnoreSubsequentWriteHeader(t *testing.T) {
   493  	handler := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   494  		w.WriteHeader(500)
   495  		w.WriteHeader(404)
   496  	}))
   497  	r := httptest.NewRequest("GET", "/", nil)
   498  	r.Header.Set("Accept-Encoding", "gzip")
   499  	w := httptest.NewRecorder()
   500  	handler.ServeHTTP(w, r)
   501  
   502  	result := w.Result()
   503  	if result.StatusCode != 500 {
   504  		t.Errorf("StatusCode should have been 500 but was %d", result.StatusCode)
   505  	}
   506  }
   507  
   508  func TestDontWriteWhenNotWrittenTo(t *testing.T) {
   509  	// When using gzip as middleware without ANY writes in the handler,
   510  	// ensure the gzip middleware doesn't touch the actual ResponseWriter
   511  	// either.
   512  
   513  	handler0 := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   514  	}))
   515  
   516  	handler1 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   517  		handler0.ServeHTTP(w, r)
   518  		w.WriteHeader(404) // this only works if gzip didn't do a WriteHeader(200)
   519  	})
   520  
   521  	r := httptest.NewRequest("GET", "/", nil)
   522  	r.Header.Set("Accept-Encoding", "gzip")
   523  	w := httptest.NewRecorder()
   524  	handler1.ServeHTTP(w, r)
   525  
   526  	result := w.Result()
   527  	if result.StatusCode != 404 {
   528  		t.Errorf("StatusCode should have been 404 but was %d", result.StatusCode)
   529  	}
   530  }
   531  
   532  var contentTypeTests = []struct {
   533  	name                 string
   534  	contentType          string
   535  	acceptedContentTypes []string
   536  	expectedGzip         bool
   537  }{
   538  	{
   539  		name:                 "Always gzip when content types are empty",
   540  		contentType:          "",
   541  		acceptedContentTypes: []string{},
   542  		expectedGzip:         true,
   543  	},
   544  	{
   545  		name:                 "MIME match",
   546  		contentType:          "application/json",
   547  		acceptedContentTypes: []string{"application/json"},
   548  		expectedGzip:         true,
   549  	},
   550  	{
   551  		name:                 "MIME no match",
   552  		contentType:          "text/xml",
   553  		acceptedContentTypes: []string{"application/json"},
   554  		expectedGzip:         false,
   555  	},
   556  	{
   557  		name:                 "MIME match with no other directive ignores non-MIME directives",
   558  		contentType:          "application/json; charset=utf-8",
   559  		acceptedContentTypes: []string{"application/json"},
   560  		expectedGzip:         true,
   561  	},
   562  	{
   563  		name:                 "MIME match with other directives requires all directives be equal, different charset",
   564  		contentType:          "application/json; charset=ascii",
   565  		acceptedContentTypes: []string{"application/json; charset=utf-8"},
   566  		expectedGzip:         false,
   567  	},
   568  	{
   569  		name:                 "MIME match with other directives requires all directives be equal, same charset",
   570  		contentType:          "application/json; charset=utf-8",
   571  		acceptedContentTypes: []string{"application/json; charset=utf-8"},
   572  		expectedGzip:         true,
   573  	},
   574  	{
   575  		name:                 "MIME match with other directives requires all directives be equal, missing charset",
   576  		contentType:          "application/json",
   577  		acceptedContentTypes: []string{"application/json; charset=ascii"},
   578  		expectedGzip:         false,
   579  	},
   580  	{
   581  		name:                 "MIME match case insensitive",
   582  		contentType:          "Application/Json",
   583  		acceptedContentTypes: []string{"application/json"},
   584  		expectedGzip:         true,
   585  	},
   586  	{
   587  		name:                 "MIME match ignore whitespace",
   588  		contentType:          "application/json;charset=utf-8",
   589  		acceptedContentTypes: []string{"application/json;            charset=utf-8"},
   590  		expectedGzip:         true,
   591  	},
   592  }
   593  
   594  func TestContentTypes(t *testing.T) {
   595  	for _, tt := range contentTypeTests {
   596  		handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   597  			w.WriteHeader(http.StatusOK)
   598  			w.Header().Set("Content-Type", tt.contentType)
   599  			_, _ = io.WriteString(w, testBody)
   600  		})
   601  
   602  		wrapper, err := GzipHandlerWithOpts(ContentTypes(tt.acceptedContentTypes))
   603  		if !assert.Nil(t, err, "NewGzipHandlerWithOpts returned error", tt.name) {
   604  			continue
   605  		}
   606  
   607  		req, _ := http.NewRequest("GET", "/whatever", nil)
   608  		req.Header.Set("Accept-Encoding", "gzip")
   609  		resp := httptest.NewRecorder()
   610  		wrapper(handler).ServeHTTP(resp, req)
   611  		res := resp.Result()
   612  
   613  		assert.Equal(t, 200, res.StatusCode)
   614  		if tt.expectedGzip {
   615  			assert.Equal(t, "gzip", res.Header.Get("Content-Encoding"), tt.name)
   616  		} else {
   617  			assert.NotEqual(t, "gzip", res.Header.Get("Content-Encoding"), tt.name)
   618  		}
   619  	}
   620  }
   621  
   622  func BenchmarkParseEncodings(b *testing.B) {
   623  	req := httptest.NewRequest(http.MethodGet, "/whatever", nil)
   624  	req.Header.Set("Accept-Encoding", strings.Repeat(",", http.DefaultMaxHeaderBytes))
   625  	b.ReportAllocs()
   626  	b.ResetTimer()
   627  	for range b.N {
   628  		_, err := parseEncodings(req.Header.Get(acceptEncoding))
   629  		assert.Error(b, err)
   630  	}
   631  }
   632  
   633  // --------------------------------------------------------------------
   634  
   635  func BenchmarkGzipHandler_S2k(b *testing.B)   { benchmark(b, false, 2048) }
   636  func BenchmarkGzipHandler_S20k(b *testing.B)  { benchmark(b, false, 20480) }
   637  func BenchmarkGzipHandler_S100k(b *testing.B) { benchmark(b, false, 102400) }
   638  func BenchmarkGzipHandler_P2k(b *testing.B)   { benchmark(b, true, 2048) }
   639  func BenchmarkGzipHandler_P20k(b *testing.B)  { benchmark(b, true, 20480) }
   640  func BenchmarkGzipHandler_P100k(b *testing.B) { benchmark(b, true, 102400) }
   641  
   642  // --------------------------------------------------------------------
   643  
   644  func gzipStrLevel(s string, lvl int) []byte {
   645  	var b bytes.Buffer
   646  	w, _ := gzip.NewWriterLevel(&b, lvl)
   647  	_, _ = io.WriteString(w, s)
   648  	w.Close()
   649  	return b.Bytes()
   650  }
   651  
   652  func benchmark(b *testing.B, parallel bool, size int) {
   653  	bin, err := os.ReadFile("testdata/benchmark.json")
   654  	if err != nil {
   655  		b.Fatal(err)
   656  	}
   657  
   658  	req, _ := http.NewRequest("GET", "/whatever", nil)
   659  	req.Header.Set("Accept-Encoding", "gzip")
   660  	handler := newTestHandler(string(bin[:size]))
   661  
   662  	if parallel {
   663  		b.ResetTimer()
   664  		b.RunParallel(func(pb *testing.PB) {
   665  			for pb.Next() {
   666  				runBenchmark(b, req, handler)
   667  			}
   668  		})
   669  	} else {
   670  		b.ResetTimer()
   671  		for i := 0; i < b.N; i++ {
   672  			runBenchmark(b, req, handler)
   673  		}
   674  	}
   675  }
   676  
   677  func runBenchmark(b *testing.B, req *http.Request, handler http.Handler) {
   678  	res := httptest.NewRecorder()
   679  	handler.ServeHTTP(res, req)
   680  	if code := res.Code; code != 200 {
   681  		b.Fatalf("Expected 200 but got %d", code)
   682  	} else if blen := res.Body.Len(); blen < 500 {
   683  		b.Fatalf("Expected complete response body, but got %d bytes", blen)
   684  	}
   685  }
   686  
   687  func newTestHandler(body string) http.Handler {
   688  	return GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   689  		switch r.URL.Path {
   690  		case "/gzipped":
   691  			w.Header().Set("Content-Encoding", "gzip")
   692  			_, _ = io.WriteString(w, body)
   693  		default:
   694  			_, _ = io.WriteString(w, body)
   695  		}
   696  	}))
   697  }