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 }