1 package gzhttp
2
3 import (
4 "bytes"
5 "context"
6 "fmt"
7 "io"
8 "math/rand"
9 "net"
10 "net/http"
11 "net/http/httptest"
12 "net/http/httptrace"
13 "net/textproto"
14 "net/url"
15 "os"
16 "strconv"
17 "strings"
18 "testing"
19
20 "github.com/klauspost/compress/gzip"
21 )
22
23 var (
24 smallTestBody = []byte("aaabbcaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbc")
25 testBody = []byte("aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc aaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbcccaaabbbccc")
26 )
27
28 func TestParseEncodings(t *testing.T) {
29 examples := map[string]codings{
30
31
32 "compress, gzip": {"compress": 1.0, "gzip": 1.0},
33 ",,,,": {},
34 "": {},
35 "*": {"*": 1.0},
36 "compress;q=0.5, gzip;q=1.0": {"compress": 0.5, "gzip": 1.0},
37 "gzip;q=1.0, identity; q=0.5, *;q=0": {"gzip": 1.0, "identity": 0.5, "*": 0.0},
38
39
40 "AAA;q=1": {"aaa": 1.0},
41 "BBB ; q = 2": {"bbb": 1.0},
42 }
43
44 for eg, exp := range examples {
45 t.Run(eg, func(t *testing.T) {
46 act, _ := parseEncodings(eg)
47 assertEqual(t, exp, act)
48 gz := parseEncodingGzip(eg)
49 assertEqual(t, exp["gzip"], gz)
50 })
51 }
52 }
53
54 func TestMustNewGzipHandler(t *testing.T) {
55
56 handler := newTestHandler(testBody)
57
58
59
60 req1, _ := http.NewRequest("GET", "/whatever", nil)
61 resp1 := httptest.NewRecorder()
62 handler.ServeHTTP(resp1, req1)
63 res1 := resp1.Result()
64
65 assertEqual(t, 200, res1.StatusCode)
66 assertEqual(t, "", res1.Header.Get("Content-Encoding"))
67 assertEqual(t, "Accept-Encoding", res1.Header.Get("Vary"))
68 assertEqual(t, testBody, resp1.Body.Bytes())
69
70
71
72 req2, _ := http.NewRequest("GET", "/whatever", nil)
73 req2.Header.Set("Accept-Encoding", "gzip")
74 resp2 := httptest.NewRecorder()
75 handler.ServeHTTP(resp2, req2)
76 res2 := resp2.Result()
77
78 assertEqual(t, 200, res2.StatusCode)
79 assertEqual(t, "gzip", res2.Header.Get("Content-Encoding"))
80 assertEqual(t, "Accept-Encoding", res2.Header.Get("Vary"))
81 assertEqual(t, gzipStrLevel(testBody, gzip.DefaultCompression), resp2.Body.Bytes())
82
83
84
85 req3, _ := http.NewRequest("GET", "/whatever", nil)
86 req3.Header.Set("Accept-Encoding", "gzip")
87 res3 := httptest.NewRecorder()
88 handler.ServeHTTP(res3, req3)
89
90 assertEqual(t, http.DetectContentType([]byte(testBody)), res3.Header().Get("Content-Type"))
91 }
92
93 func TestGzipHandlerSmallBodyNoCompression(t *testing.T) {
94 handler := newTestHandler(smallTestBody)
95
96 req, _ := http.NewRequest("GET", "/whatever", nil)
97 req.Header.Set("Accept-Encoding", "gzip")
98 resp := httptest.NewRecorder()
99 handler.ServeHTTP(resp, req)
100 res := resp.Result()
101
102
103
104 assertEqual(t, 200, res.StatusCode)
105 assertEqual(t, "", res.Header.Get("Content-Encoding"))
106 assertEqual(t, "Accept-Encoding", res.Header.Get("Vary"))
107 assertEqual(t, smallTestBody, resp.Body.Bytes())
108
109 }
110
111 func TestGzipHandlerAlreadyCompressed(t *testing.T) {
112 handler := newTestHandler(testBody)
113
114 req, _ := http.NewRequest("GET", "/gzipped", nil)
115 req.Header.Set("Accept-Encoding", "gzip")
116 res := httptest.NewRecorder()
117 handler.ServeHTTP(res, req)
118
119 assertEqual(t, testBody, res.Body.Bytes())
120 }
121
122 func TestGzipHandlerRangeReply(t *testing.T) {
123 handler := GzipHandler(
124 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
125 w.Header().Set("Content-Range", "bytes 0-300/804")
126 w.WriteHeader(http.StatusOK)
127 w.Write([]byte(testBody))
128 }))
129 req, _ := http.NewRequest("GET", "/gzipped", nil)
130 req.Header.Set("Accept-Encoding", "gzip")
131
132 resp := httptest.NewRecorder()
133 handler.ServeHTTP(resp, req)
134 res := resp.Result()
135 assertEqual(t, 200, res.StatusCode)
136 assertEqual(t, "", res.Header.Get("Content-Encoding"))
137 assertEqual(t, testBody, resp.Body.Bytes())
138 }
139
140 func TestGzipHandlerAcceptRange(t *testing.T) {
141 handler := GzipHandler(
142 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
143 w.Header().Set("Accept-Ranges", "bytes")
144 w.WriteHeader(http.StatusOK)
145 w.Write([]byte(testBody))
146 }))
147 req, _ := http.NewRequest("GET", "/gzipped", nil)
148 req.Header.Set("Accept-Encoding", "gzip")
149
150 resp := httptest.NewRecorder()
151 handler.ServeHTTP(resp, req)
152 res := resp.Result()
153 assertEqual(t, 200, res.StatusCode)
154 assertEqual(t, "gzip", res.Header.Get("Content-Encoding"))
155 assertEqual(t, "", res.Header.Get("Accept-Ranges"))
156 zr, err := gzip.NewReader(resp.Body)
157 assertNil(t, err)
158 got, err := io.ReadAll(zr)
159 assertNil(t, err)
160 assertEqual(t, testBody, got)
161 }
162
163 func TestGzipHandlerKeepAcceptRange(t *testing.T) {
164 wrapper, err := NewWrapper(KeepAcceptRanges())
165 assertNil(t, err)
166 handler := wrapper(
167 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
168 w.Header().Set("Accept-Ranges", "bytes")
169 w.WriteHeader(http.StatusOK)
170 w.Write([]byte(testBody))
171 }))
172 req, _ := http.NewRequest("GET", "/gzipped", nil)
173 req.Header.Set("Accept-Encoding", "gzip")
174
175 resp := httptest.NewRecorder()
176 handler.ServeHTTP(resp, req)
177 res := resp.Result()
178 assertEqual(t, 200, res.StatusCode)
179 assertEqual(t, "gzip", res.Header.Get("Content-Encoding"))
180 assertEqual(t, "bytes", res.Header.Get("Accept-Ranges"))
181 zr, err := gzip.NewReader(resp.Body)
182 assertNil(t, err)
183 got, err := io.ReadAll(zr)
184 assertNil(t, err)
185 assertEqual(t, testBody, got)
186 }
187
188 func TestGzipHandlerSuffixETag(t *testing.T) {
189 wrapper, err := NewWrapper(SuffixETag("-gzip"))
190 assertNil(t, err)
191
192 handlerWithETag := wrapper(
193 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
194 w.Header().Set("ETag", `W/"1234"`)
195 w.WriteHeader(http.StatusOK)
196 w.Write([]byte(testBody))
197 }))
198 handlerWithoutETag := wrapper(
199 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
200 w.WriteHeader(http.StatusOK)
201 w.Write([]byte(testBody))
202 }))
203
204 req, _ := http.NewRequest("GET", "/gzipped", nil)
205 req.Header.Set("Accept-Encoding", "gzip")
206
207 respWithEtag := httptest.NewRecorder()
208 respWithoutEtag := httptest.NewRecorder()
209 handlerWithETag.ServeHTTP(respWithEtag, req)
210 handlerWithoutETag.ServeHTTP(respWithoutEtag, req)
211
212 resWithEtag := respWithEtag.Result()
213 assertEqual(t, 200, resWithEtag.StatusCode)
214 assertEqual(t, "gzip", resWithEtag.Header.Get("Content-Encoding"))
215 assertEqual(t, `W/"1234-gzip"`, resWithEtag.Header.Get("ETag"))
216 zr, err := gzip.NewReader(resWithEtag.Body)
217 assertNil(t, err)
218 got, err := io.ReadAll(zr)
219 assertNil(t, err)
220 assertEqual(t, testBody, got)
221
222 resWithoutEtag := respWithoutEtag.Result()
223 assertEqual(t, 200, resWithoutEtag.StatusCode)
224 assertEqual(t, "gzip", resWithoutEtag.Header.Get("Content-Encoding"))
225 assertEqual(t, "", resWithoutEtag.Header.Get("ETag"))
226 zr, err = gzip.NewReader(resWithoutEtag.Body)
227 assertNil(t, err)
228 got, err = io.ReadAll(zr)
229 assertNil(t, err)
230 assertEqual(t, testBody, got)
231 }
232
233 func TestGzipHandlerDropETag(t *testing.T) {
234 wrapper, err := NewWrapper(DropETag())
235 assertNil(t, err)
236
237 handlerCompressed := wrapper(
238 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
239 w.Header().Set("ETag", `W/"1234"`)
240 w.WriteHeader(http.StatusOK)
241 w.Write([]byte(testBody))
242 }))
243 handlerUncompressed := wrapper(
244 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
245 w.Header().Set("ETag", `W/"1234"`)
246 w.Header().Set(HeaderNoCompression, "true")
247 w.WriteHeader(http.StatusOK)
248 w.Write([]byte(testBody))
249 }))
250
251 req, _ := http.NewRequest("GET", "/gzipped", nil)
252 req.Header.Set("Accept-Encoding", "gzip")
253
254 respCompressed := httptest.NewRecorder()
255 respUncompressed := httptest.NewRecorder()
256 handlerCompressed.ServeHTTP(respCompressed, req)
257 handlerUncompressed.ServeHTTP(respUncompressed, req)
258
259 resCompressed := respCompressed.Result()
260 assertEqual(t, 200, resCompressed.StatusCode)
261 assertEqual(t, "gzip", resCompressed.Header.Get("Content-Encoding"))
262 assertEqual(t, "", resCompressed.Header.Get("ETag"))
263 zr, err := gzip.NewReader(resCompressed.Body)
264 assertNil(t, err)
265 got, err := io.ReadAll(zr)
266 assertNil(t, err)
267 assertEqual(t, testBody, got)
268
269 resUncompressed := respUncompressed.Result()
270 assertEqual(t, 200, resUncompressed.StatusCode)
271 assertEqual(t, "", resUncompressed.Header.Get("Content-Encoding"))
272 assertEqual(t, `W/"1234"`, resUncompressed.Header.Get("ETag"))
273 got, err = io.ReadAll(resUncompressed.Body)
274 assertNil(t, err)
275 assertEqual(t, testBody, got)
276 }
277
278 func TestNewGzipLevelHandler(t *testing.T) {
279 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
280 w.WriteHeader(http.StatusOK)
281 w.Write(testBody)
282 })
283
284 for lvl := gzip.StatelessCompression; lvl <= gzip.BestCompression; lvl++ {
285 t.Run(fmt.Sprint(lvl), func(t *testing.T) {
286 wrapper, err := NewWrapper(CompressionLevel(lvl))
287 assertNil(t, err)
288
289 req, _ := http.NewRequest("GET", "/whatever", nil)
290 req.Header.Set("Accept-Encoding", "gzip")
291 resp := httptest.NewRecorder()
292 wrapper(handler).ServeHTTP(resp, req)
293 res := resp.Result()
294
295 assertEqual(t, 200, res.StatusCode)
296 assertEqual(t, "gzip", res.Header.Get("Content-Encoding"))
297 assertEqual(t, "Accept-Encoding", res.Header.Get("Vary"))
298 got := gzipStrLevel(testBody, lvl)
299 if lvl != gzip.StatelessCompression {
300 assertEqual(t, got, resp.Body.Bytes())
301 }
302 t.Log(lvl, len(got))
303 })
304 }
305 }
306
307 func TestNewGzipLevelHandlerReturnsErrorForInvalidLevels(t *testing.T) {
308 var err error
309 _, err = NewWrapper(CompressionLevel(-42))
310 assertNotNil(t, err)
311
312 _, err = NewWrapper(CompressionLevel(42))
313 assertNotNil(t, err)
314 }
315
316 func TestMustNewGzipLevelHandlerWillPanic(t *testing.T) {
317 defer func() {
318 if r := recover(); r != nil {
319 t.Error("panic was called with", r)
320 }
321 }()
322
323 _ = GzipHandler(nil)
324 }
325
326 func TestGzipHandlerNoBody(t *testing.T) {
327 tests := []struct {
328 statusCode int
329 contentEncoding string
330 emptyBody bool
331 body []byte
332 }{
333
334 {http.StatusNoContent, "", true, nil},
335 {http.StatusNotModified, "", true, nil},
336
337 {http.StatusOK, "", true, []byte{}},
338 {http.StatusOK, "gzip", false, []byte(testBody)},
339 }
340
341 for num, test := range tests {
342 t.Run(fmt.Sprintf("test-%d", num), func(t *testing.T) {
343 handler := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
344 w.WriteHeader(test.statusCode)
345 if test.body != nil {
346 w.Write(test.body)
347 }
348 }))
349
350 rec := httptest.NewRecorder()
351 req := httptest.NewRequest(http.MethodGet, "/", nil)
352 req.Header.Set("Accept-Encoding", "gzip")
353 handler.ServeHTTP(rec, req)
354
355 body, err := io.ReadAll(rec.Body)
356 if err != nil {
357 t.Fatalf("Unexpected error reading response body: %v", err)
358 }
359
360 header := rec.Header()
361 assertEqual(t, test.contentEncoding, header.Get("Content-Encoding"))
362 assertEqual(t, "Accept-Encoding", header.Get("Vary"))
363 if test.emptyBody {
364 assertEqual(t, 0, len(body))
365 } else {
366 assertNotEqual(t, 0, len(body))
367 assertNotEqual(t, test.body, body)
368 }
369 })
370
371 }
372 }
373
374 func TestGzipHandlerContentLength(t *testing.T) {
375 testBodyBytes := []byte(testBody)
376 tests := []struct {
377 bodyLen int
378 bodies [][]byte
379 emptyBody bool
380 }{
381 {len(testBody), [][]byte{testBodyBytes}, false},
382
383 {len(testBody), [][]byte{testBodyBytes[:200], testBodyBytes[200:]}, false},
384
385 {0, [][]byte{testBodyBytes[:200], testBodyBytes[200:]}, false},
386
387 {len(testBody), [][]byte{nil}, true},
388 }
389
390
391
392
393 ln, err := net.Listen("tcp", "localhost:")
394 if err != nil {
395 t.Fatalf("failed creating listen socket: %v", err)
396 }
397 defer ln.Close()
398 srv := &http.Server{
399 Handler: nil,
400 }
401 go srv.Serve(ln)
402
403 for num, test := range tests {
404 t.Run(fmt.Sprintf("test-%d", num), func(t *testing.T) {
405 srv.Handler = GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
406 if test.bodyLen > 0 {
407 w.Header().Set("Content-Length", strconv.Itoa(test.bodyLen))
408 }
409 for _, b := range test.bodies {
410 w.Write(b)
411 }
412 }))
413 req := &http.Request{
414 Method: "GET",
415 URL: &url.URL{Path: "/", Scheme: "http", Host: ln.Addr().String()},
416 Header: make(http.Header),
417 Close: true,
418 }
419 req.Header.Set("Accept-Encoding", "gzip")
420 res, err := http.DefaultClient.Do(req)
421 if err != nil {
422 t.Fatalf("Unexpected error making http request in test iteration %d: %v", num, err)
423 }
424 defer res.Body.Close()
425
426 body, err := io.ReadAll(res.Body)
427 if err != nil {
428 t.Fatalf("Unexpected error reading response body in test iteration %d: %v", num, err)
429 }
430
431 l, err := strconv.Atoi(res.Header.Get("Content-Length"))
432 if err != nil {
433 t.Fatalf("Unexpected error parsing Content-Length in test iteration %d: %v", num, err)
434 }
435 if test.emptyBody {
436 assertEqual(t, 0, len(body))
437 assertEqual(t, 0, l)
438 } else {
439 assertEqual(t, len(body), l)
440 }
441 assertEqual(t, "gzip", res.Header.Get("Content-Encoding"))
442 assertNotEqual(t, test.bodyLen, l)
443 })
444 }
445 }
446
447 func TestGzipHandlerMinSizeMustBePositive(t *testing.T) {
448 _, err := NewWrapper(MinSize(-1))
449 assertNotNil(t, err)
450 }
451
452 func TestGzipHandlerMinSize(t *testing.T) {
453 responseLength := 0
454 b := []byte{'x'}
455
456 wrapper, _ := NewWrapper(MinSize(128))
457 handler := wrapper(http.HandlerFunc(
458 func(w http.ResponseWriter, r *http.Request) {
459
460
461 for i := 0; i < responseLength; i++ {
462 n, err := w.Write(b)
463 assertEqual(t, 1, n)
464 assertNil(t, err)
465 }
466 },
467 ))
468
469 r, _ := http.NewRequest("GET", "/whatever", &bytes.Buffer{})
470 r.Header.Add("Accept-Encoding", "gzip")
471
472
473 responseLength = 127
474 w := httptest.NewRecorder()
475 handler.ServeHTTP(w, r)
476 if w.Result().Header.Get(contentEncoding) == "gzip" {
477 t.Error("Expected uncompressed response, got compressed")
478 }
479
480
481 responseLength = 128
482 w = httptest.NewRecorder()
483 handler.ServeHTTP(w, r)
484 if w.Result().Header.Get(contentEncoding) != "gzip" {
485 t.Error("Expected compressed response, got uncompressed")
486 }
487 }
488
489 type panicOnSecondWriteHeaderWriter struct {
490 http.ResponseWriter
491 headerWritten bool
492 }
493
494 func (w *panicOnSecondWriteHeaderWriter) WriteHeader(s int) {
495 if w.headerWritten {
496 panic("header already written")
497 }
498 w.headerWritten = true
499 w.ResponseWriter.WriteHeader(s)
500 }
501
502 func TestGzipHandlerDoubleWriteHeader(t *testing.T) {
503 handler := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
504 w.Header().Set("Content-Length", "15000")
505
506 w.WriteHeader(304)
507
508 w.Write(nil)
509 }))
510 wrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
511 w = &panicOnSecondWriteHeaderWriter{
512 ResponseWriter: w,
513 }
514 handler.ServeHTTP(w, r)
515 })
516
517 rec := httptest.NewRecorder()
518
519
520 req := &http.Request{
521 Method: "GET",
522 URL: &url.URL{Path: "/"},
523 Proto: "HTTP/1.1",
524 ProtoMinor: 1,
525 RemoteAddr: "192.0.2.1:1234",
526 Header: make(http.Header),
527 }
528 req.Header.Set("Accept-Encoding", "gzip")
529 wrapper.ServeHTTP(rec, req)
530 body, err := io.ReadAll(rec.Body)
531 if err != nil {
532 t.Fatalf("Unexpected error reading response body: %v", err)
533 }
534 assertEqual(t, 0, len(body))
535 header := rec.Header()
536 assertEqual(t, "gzip", header.Get("Content-Encoding"))
537 assertEqual(t, "Accept-Encoding", header.Get("Vary"))
538 assertEqual(t, 304, rec.Code)
539 }
540
541 func TestStatusCodes(t *testing.T) {
542 handler := GzipHandler(http.NotFoundHandler())
543 r := httptest.NewRequest("GET", "/", nil)
544 r.Header.Set("Accept-Encoding", "gzip")
545 w := httptest.NewRecorder()
546 handler.ServeHTTP(w, r)
547
548 result := w.Result()
549 if result.StatusCode != 404 {
550 t.Errorf("StatusCode should have been 404 but was %d", result.StatusCode)
551 }
552 }
553
554 func TestFlushBeforeWrite(t *testing.T) {
555 b := []byte(testBody)
556 handler := GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
557 rw.WriteHeader(http.StatusNotFound)
558 rw.(http.Flusher).Flush()
559 rw.Write(b)
560 }))
561 r := httptest.NewRequest(http.MethodGet, "/", nil)
562 r.Header.Set("Accept-Encoding", "gzip")
563 w := httptest.NewRecorder()
564 handler.ServeHTTP(w, r)
565
566 res := w.Result()
567 assertEqual(t, http.StatusNotFound, res.StatusCode)
568 assertEqual(t, "gzip", res.Header.Get("Content-Encoding"))
569 assertNotEqual(t, b, w.Body.Bytes())
570 }
571
572 func TestFlushAfterWrite(t *testing.T) {
573 b := testBody[:1000]
574 handler := GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
575 rw.WriteHeader(http.StatusOK)
576 rw.Write(b[0:1])
577 rw.(http.Flusher).Flush()
578 for i := range b[1:] {
579 rw.Write(b[i+1 : i+2])
580 }
581 }))
582 r := httptest.NewRequest(http.MethodGet, "/", nil)
583 r.Header.Set("Accept-Encoding", "gzip")
584 w := httptest.NewRecorder()
585 handler.ServeHTTP(w, r)
586
587 res := w.Result()
588 assertEqual(t, http.StatusOK, res.StatusCode)
589 assertEqual(t, "gzip", res.Header.Get("Content-Encoding"))
590 gr, err := gzip.NewReader(w.Body)
591 assertNil(t, err)
592 got, err := io.ReadAll(gr)
593 assertNil(t, err)
594 assertEqual(t, b, got)
595 }
596
597 func TestFlushAfterWrite2(t *testing.T) {
598 b := testBody[:1050]
599 handler := GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
600 for i := range b {
601 rw.Write(b[i : i+1])
602 }
603 rw.(http.Flusher).Flush()
604 }))
605 r := httptest.NewRequest(http.MethodGet, "/", nil)
606 r.Header.Set("Accept-Encoding", "gzip")
607 w := httptest.NewRecorder()
608 handler.ServeHTTP(w, r)
609
610 res := w.Result()
611 assertEqual(t, http.StatusOK, res.StatusCode)
612 assertEqual(t, "gzip", res.Header.Get("Content-Encoding"))
613 gr, err := gzip.NewReader(w.Body)
614 assertNil(t, err)
615 got, err := io.ReadAll(gr)
616 assertNil(t, err)
617 assertEqual(t, b, got)
618 }
619
620 func TestFlushAfterWrite3(t *testing.T) {
621 b := []byte(nil)
622 gz, err := NewWrapper(MinSize(1000), CompressionLevel(gzip.BestSpeed))
623 if err != nil {
624
625 t.Fatal(err, "Unable to initialize server")
626 }
627 handler := gz(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
628 rw.WriteHeader(http.StatusOK)
629
630 rw.(http.Flusher).Flush()
631 }))
632 r := httptest.NewRequest(http.MethodGet, "/", nil)
633 r.Header.Set("Accept-Encoding", "gzip")
634 w := httptest.NewRecorder()
635 handler.ServeHTTP(w, r)
636
637 res := w.Result()
638 assertEqual(t, http.StatusOK, res.StatusCode)
639 assertEqual(t, "", res.Header.Get("Content-Encoding"))
640 assertEqual(t, b, w.Body.Bytes())
641 }
642
643 func TestImplementCloseNotifier(t *testing.T) {
644 request := httptest.NewRequest(http.MethodGet, "/", nil)
645 request.Header.Set(acceptEncoding, "gzip")
646 GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
647 _, ok := rw.(http.CloseNotifier)
648
649 assertEqual(t, true, ok)
650 })).ServeHTTP(&mockRWCloseNotify{}, request)
651 }
652
653 func TestImplementFlusherAndCloseNotifier(t *testing.T) {
654 request := httptest.NewRequest(http.MethodGet, "/", nil)
655 request.Header.Set(acceptEncoding, "gzip")
656 GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
657 _, okCloseNotifier := rw.(http.CloseNotifier)
658
659 assertEqual(t, true, okCloseNotifier)
660 _, okFlusher := rw.(http.Flusher)
661
662 assertEqual(t, true, okFlusher)
663 })).ServeHTTP(&mockRWCloseNotify{}, request)
664 }
665
666 func TestNotImplementCloseNotifier(t *testing.T) {
667 request := httptest.NewRequest(http.MethodGet, "/", nil)
668 request.Header.Set(acceptEncoding, "gzip")
669 GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
670 _, ok := rw.(http.CloseNotifier)
671
672 assertEqual(t, false, ok)
673 })).ServeHTTP(httptest.NewRecorder(), request)
674 }
675
676 type mockRWCloseNotify struct{}
677
678 func (m *mockRWCloseNotify) CloseNotify() <-chan bool {
679 panic("implement me")
680 }
681
682 func (m *mockRWCloseNotify) Header() http.Header {
683 return http.Header{}
684 }
685
686 func (m *mockRWCloseNotify) Write([]byte) (int, error) {
687 panic("implement me")
688 }
689
690 func (m *mockRWCloseNotify) WriteHeader(int) {
691 panic("implement me")
692 }
693
694 func TestIgnoreSubsequentWriteHeader(t *testing.T) {
695 handler := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
696 w.WriteHeader(500)
697 w.WriteHeader(404)
698 }))
699 r := httptest.NewRequest("GET", "/", nil)
700 r.Header.Set("Accept-Encoding", "gzip")
701 w := httptest.NewRecorder()
702 handler.ServeHTTP(w, r)
703
704 result := w.Result()
705 if result.StatusCode != 500 {
706 t.Errorf("StatusCode should have been 500 but was %d", result.StatusCode)
707 }
708 }
709
710 func TestDontWriteWhenNotWrittenTo(t *testing.T) {
711
712
713
714
715 handler0 := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
716 }))
717
718 handler1 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
719 handler0.ServeHTTP(w, r)
720 w.WriteHeader(404)
721 })
722
723 r := httptest.NewRequest("GET", "/", nil)
724 r.Header.Set("Accept-Encoding", "gzip")
725 w := httptest.NewRecorder()
726 handler1.ServeHTTP(w, r)
727
728 result := w.Result()
729 if result.StatusCode != 404 {
730 t.Errorf("StatusCode should have been 404 but was %d", result.StatusCode)
731 }
732 }
733
734 var contentTypeTests = []struct {
735 name string
736 contentType string
737 acceptedContentTypes []string
738 expectedGzip bool
739 }{
740 {
741 name: "Always gzip when content types are empty",
742 contentType: "",
743 acceptedContentTypes: []string{},
744 expectedGzip: true,
745 },
746 {
747 name: "MIME match",
748 contentType: "application/json",
749 acceptedContentTypes: []string{"application/json"},
750 expectedGzip: true,
751 },
752 {
753 name: "MIME no match",
754 contentType: "text/xml",
755 acceptedContentTypes: []string{"application/json"},
756 expectedGzip: false,
757 },
758 {
759 name: "MIME match with no other directive ignores non-MIME directives",
760 contentType: "application/json; charset=utf-8",
761 acceptedContentTypes: []string{"application/json"},
762 expectedGzip: true,
763 },
764 {
765 name: "MIME match with other directives requires all directives be equal, different charset",
766 contentType: "application/json; charset=ascii",
767 acceptedContentTypes: []string{"application/json; charset=utf-8"},
768 expectedGzip: false,
769 },
770 {
771 name: "MIME match with other directives requires all directives be equal, same charset",
772 contentType: "application/json; charset=utf-8",
773 acceptedContentTypes: []string{"application/json; charset=utf-8"},
774 expectedGzip: true,
775 },
776 {
777 name: "MIME match with other directives requires all directives be equal, missing charset",
778 contentType: "application/json",
779 acceptedContentTypes: []string{"application/json; charset=ascii"},
780 expectedGzip: false,
781 },
782 {
783 name: "MIME match case insensitive",
784 contentType: "Application/Json",
785 acceptedContentTypes: []string{"application/json"},
786 expectedGzip: true,
787 },
788 {
789 name: "MIME match ignore whitespace",
790 contentType: "application/json;charset=utf-8",
791 acceptedContentTypes: []string{"application/json; charset=utf-8"},
792 expectedGzip: true,
793 },
794 }
795
796 func TestContentTypes(t *testing.T) {
797 for _, tt := range contentTypeTests {
798 t.Run(tt.name, func(t *testing.T) {
799 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
800 w.WriteHeader(http.StatusOK)
801 w.Header().Set("Content-Type", tt.contentType)
802 w.Write(testBody)
803 })
804
805 wrapper, err := NewWrapper(ContentTypes(tt.acceptedContentTypes))
806 assertNil(t, err)
807
808 req, _ := http.NewRequest("GET", "/whatever", nil)
809 req.Header.Set("Accept-Encoding", "gzip")
810 resp := httptest.NewRecorder()
811 wrapper(handler).ServeHTTP(resp, req)
812 res := resp.Result()
813
814 assertEqual(t, 200, res.StatusCode)
815 if tt.expectedGzip {
816 assertEqual(t, "gzip", res.Header.Get("Content-Encoding"))
817 } else {
818 assertNotEqual(t, "gzip", res.Header.Get("Content-Encoding"))
819 }
820 })
821 t.Run("not-"+tt.name, func(t *testing.T) {
822 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
823 w.WriteHeader(http.StatusOK)
824 w.Header().Set("Content-Type", tt.contentType)
825 w.Write(testBody)
826 })
827
828 wrapper, err := NewWrapper(ExceptContentTypes(tt.acceptedContentTypes))
829 assertNil(t, err)
830
831 req, _ := http.NewRequest("GET", "/whatever", nil)
832 req.Header.Set("Accept-Encoding", "gzip")
833 resp := httptest.NewRecorder()
834 wrapper(handler).ServeHTTP(resp, req)
835 res := resp.Result()
836
837 assertEqual(t, 200, res.StatusCode)
838 if !tt.expectedGzip {
839 assertEqual(t, "gzip", res.Header.Get("Content-Encoding"))
840 } else {
841 assertNotEqual(t, "gzip", res.Header.Get("Content-Encoding"))
842 }
843 })
844 t.Run("disable-"+tt.name, func(t *testing.T) {
845 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
846 w.Header().Set("Content-Type", tt.contentType)
847 w.Header().Set(HeaderNoCompression, "plz")
848 w.WriteHeader(http.StatusOK)
849 w.Write(testBody)
850 })
851
852 wrapper, err := NewWrapper(ContentTypes(tt.acceptedContentTypes))
853 assertNil(t, err)
854
855 req, _ := http.NewRequest("GET", "/whatever", nil)
856 req.Header.Set("Accept-Encoding", "gzip")
857 resp := httptest.NewRecorder()
858 wrapper(handler).ServeHTTP(resp, req)
859 res := resp.Result()
860
861 assertEqual(t, 200, res.StatusCode)
862 assertNotEqual(t, "gzip", res.Header.Get("Content-Encoding"))
863 _, ok := res.Header[HeaderNoCompression]
864 assertEqual(t, false, ok)
865 })
866 t.Run("head-req"+tt.name, func(t *testing.T) {
867 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
868 w.Header().Set("Content-Type", tt.contentType)
869 w.Header().Set(HeaderNoCompression, "plz")
870 w.WriteHeader(http.StatusOK)
871 })
872
873 wrapper, err := NewWrapper(ContentTypes(tt.acceptedContentTypes))
874 assertNil(t, err)
875
876 req, _ := http.NewRequest("HEAD", "/whatever", nil)
877 req.Header.Set("Accept-Encoding", "gzip")
878 resp := httptest.NewRecorder()
879 wrapper(handler).ServeHTTP(resp, req)
880 res := resp.Result()
881
882 assertEqual(t, 200, res.StatusCode)
883 assertNotEqual(t, "gzip", res.Header.Get("Content-Encoding"))
884 _, ok := res.Header[HeaderNoCompression]
885 assertEqual(t, false, ok)
886 })
887 t.Run("head-req-no-ok"+tt.name, func(t *testing.T) {
888 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
889 w.Header().Set("Content-Type", tt.contentType)
890 w.Header().Set(HeaderNoCompression, "plz")
891 })
892
893 wrapper, err := NewWrapper(ContentTypes(tt.acceptedContentTypes))
894 assertNil(t, err)
895
896 req, _ := http.NewRequest("HEAD", "/whatever", nil)
897 req.Header.Set("Accept-Encoding", "gzip")
898 resp := httptest.NewRecorder()
899 wrapper(handler).ServeHTTP(resp, req)
900 res := resp.Result()
901
902 assertEqual(t, 200, res.StatusCode)
903 assertNotEqual(t, "gzip", res.Header.Get("Content-Encoding"))
904 _, ok := res.Header[HeaderNoCompression]
905 assertEqual(t, false, ok)
906 })
907 t.Run("req-no-ok-write"+tt.name, func(t *testing.T) {
908 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
909 w.Header().Set("Content-Type", tt.contentType)
910 w.Header().Set(HeaderNoCompression, "plz")
911 w.Write(testBody)
912 })
913
914 wrapper, err := NewWrapper(ContentTypes(tt.acceptedContentTypes))
915 assertNil(t, err)
916
917 req, _ := http.NewRequest("GET", "/whatever", nil)
918 req.Header.Set("Accept-Encoding", "")
919 resp := httptest.NewRecorder()
920 wrapper(handler).ServeHTTP(resp, req)
921 res := resp.Result()
922
923 assertEqual(t, 200, res.StatusCode)
924 assertNotEqual(t, "gzip", res.Header.Get("Content-Encoding"))
925 _, ok := res.Header[HeaderNoCompression]
926 assertEqual(t, false, ok)
927 })
928 }
929 }
930
931 func TestFlush(t *testing.T) {
932 for _, tt := range contentTypeTests {
933 t.Run(tt.name, func(t *testing.T) {
934 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
935 w.WriteHeader(http.StatusOK)
936 w.Header().Set("Content-Type", tt.contentType)
937 tb := testBody
938 for len(tb) > 0 {
939
940
941 toWrite := 100
942 if toWrite > len(tb) {
943 toWrite = len(tb)
944 }
945 _, err := w.Write(tb[:toWrite])
946 if err != nil {
947 t.Fatal(err)
948 }
949
950 w.(http.Flusher).Flush()
951 tb = tb[toWrite:]
952 }
953 })
954
955 wrapper, err := NewWrapper(ContentTypes(tt.acceptedContentTypes))
956 assertNil(t, err)
957
958 req, _ := http.NewRequest("GET", "/whatever", nil)
959 req.Header.Set("Accept-Encoding", "gzip")
960
961 resp := httptest.NewRecorder()
962 wrapper(handler).ServeHTTP(resp, req)
963 res := resp.Result()
964
965 assertEqual(t, 200, res.StatusCode)
966 if tt.expectedGzip {
967 assertEqual(t, "gzip", res.Header.Get("Content-Encoding"))
968 zr, err := gzip.NewReader(resp.Body)
969 assertNil(t, err)
970 got, err := io.ReadAll(zr)
971 assertNil(t, err)
972 assertEqual(t, testBody, got)
973
974 } else {
975 assertNotEqual(t, "gzip", res.Header.Get("Content-Encoding"))
976 got, err := io.ReadAll(resp.Body)
977 assertNil(t, err)
978 assertEqual(t, testBody, got)
979 }
980 })
981 }
982 }
983
984 func TestRandomJitter(t *testing.T) {
985 r := httptest.NewRequest("GET", "/", nil)
986 r.Header.Set("Accept-Encoding", "gzip")
987
988
989 rng := rand.New(rand.NewSource(0))
990 payload := make([]byte, 4096)
991 _, err := io.ReadFull(rng, payload)
992 if err != nil {
993 t.Fatal(err)
994 }
995
996 wrapper, err := NewWrapper(RandomJitter(256, 1024, false), MinSize(10))
997 if err != nil {
998 t.Fatal(err)
999 }
1000 writePayload := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1001 w.Write(payload)
1002 })
1003 referenceHandler := GzipHandler(writePayload)
1004 w := httptest.NewRecorder()
1005 referenceHandler.ServeHTTP(w, r)
1006 result := w.Result()
1007 refBody, err := io.ReadAll(result.Body)
1008 if err != nil {
1009 t.Fatal(err)
1010 }
1011 t.Logf("Unmodified length: %d", len(refBody))
1012
1013 handler := wrapper(writePayload)
1014 w = httptest.NewRecorder()
1015 handler.ServeHTTP(w, r)
1016
1017 result = w.Result()
1018 b, err := io.ReadAll(result.Body)
1019 if err != nil {
1020 t.Fatal(err)
1021 }
1022
1023 if len(refBody) == len(b) {
1024 t.Fatal("padding was not applied")
1025 }
1026
1027 if err != nil {
1028 t.Fatal(err)
1029 }
1030 changed := false
1031 for i := 0; i < 10; i++ {
1032 w = httptest.NewRecorder()
1033 handler.ServeHTTP(w, r)
1034 result = w.Result()
1035 b2, err := io.ReadAll(result.Body)
1036 if err != nil {
1037 t.Fatal(err)
1038 }
1039 changed = changed || len(b2) != len(b)
1040 t.Logf("attempt %d length: %d. padding: %d.", i, len(b2), len(b2)-len(refBody))
1041 if len(b2) <= len(refBody) {
1042 t.Errorf("no padding applied,")
1043 }
1044 if i == 0 && changed {
1045 t.Error("length changed without payload change", len(b), "->", len(b2))
1046 }
1047
1048 payload[0]++
1049 b = b2
1050 }
1051 if !changed {
1052 t.Errorf("no change after 9 attempts")
1053 }
1054
1055
1056 handler = wrapper(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1057 for i := range payload {
1058 w.Write([]byte{payload[i]})
1059 }
1060 }))
1061
1062 for i := 0; i < 10; i++ {
1063 w = httptest.NewRecorder()
1064 handler.ServeHTTP(w, r)
1065 result = w.Result()
1066 b2, err := io.ReadAll(result.Body)
1067 if err != nil {
1068 t.Fatal(err)
1069 }
1070
1071 t.Logf("attempt %d length: %d. padding: %d.", i, len(b2), len(b2)-len(refBody))
1072 if len(b2) <= len(refBody) {
1073 t.Errorf("no padding applied,")
1074 }
1075 if i > 0 && len(b2) != len(b) {
1076 t.Error("length changed without payload change", len(b), "->", len(b2))
1077 }
1078
1079 payload[2048]++
1080 b = b2
1081 }
1082
1083
1084 handler = wrapper(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1085 w.Write(payload[:512])
1086 }))
1087 changed = false
1088 for i := 0; i < 10; i++ {
1089 w = httptest.NewRecorder()
1090 handler.ServeHTTP(w, r)
1091 result = w.Result()
1092 b2, err := io.ReadAll(result.Body)
1093 if err != nil {
1094 t.Fatal(err)
1095 }
1096 if i > 0 {
1097 changed = changed || len(b2) != len(b)
1098 }
1099 t.Logf("attempt %d length: %d. padding: %d.", i, len(b2), len(b2)-512)
1100 if len(b2) <= 512 {
1101 t.Errorf("no padding applied,")
1102 }
1103
1104 payload[500]++
1105 b = b2
1106 }
1107 if !changed {
1108 t.Errorf("no change after 9 attempts")
1109 }
1110
1111
1112
1113 handler = wrapper(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1114 w.Write(payload[:256])
1115 w.(http.Flusher).Flush()
1116 w.Write(payload[256:512])
1117 }))
1118
1119 changed = false
1120 for i := 0; i < 10; i++ {
1121 w = httptest.NewRecorder()
1122 handler.ServeHTTP(w, r)
1123 result = w.Result()
1124 b2, err := io.ReadAll(result.Body)
1125 if err != nil {
1126 t.Fatal(err)
1127 }
1128 if i > 0 {
1129 changed = changed || len(b2) != len(b)
1130 }
1131 t.Logf("attempt %d length: %d. padding: %d.", i, len(b2), len(b2)-512)
1132 if len(b2) <= 512 {
1133 t.Errorf("no padding applied,")
1134 }
1135
1136 payload[200]++
1137 b = b2
1138 }
1139 if !changed {
1140 t.Errorf("no change after 9 attempts")
1141 }
1142
1143
1144
1145 for i := 0; i < 10; i++ {
1146 w = httptest.NewRecorder()
1147 handler.ServeHTTP(w, r)
1148 result = w.Result()
1149 b2, err := io.ReadAll(result.Body)
1150 if err != nil {
1151 t.Fatal(err)
1152 }
1153 if i > 0 {
1154 changed = len(b2) != len(b)
1155 if changed {
1156 t.Errorf("mutating after flush seems to have affected output")
1157 }
1158 }
1159 t.Logf("attempt %d length: %d. padding: %d.", i, len(b2), len(b2)-512)
1160 if len(b2) <= 512 {
1161 t.Errorf("no padding applied,")
1162 }
1163
1164 payload[400]++
1165 b = b2
1166 }
1167
1168
1169 wrapper, err = NewWrapper(RandomJitter(256, -1, false), MinSize(10))
1170 if err != nil {
1171 t.Fatal(err)
1172 }
1173 handler = wrapper(writePayload)
1174 changed = false
1175 for i := 0; i < 10; i++ {
1176 w = httptest.NewRecorder()
1177 handler.ServeHTTP(w, r)
1178 result = w.Result()
1179 b2, err := io.ReadAll(result.Body)
1180 if err != nil {
1181 t.Fatal(err)
1182 }
1183 if i > 0 {
1184 changed = changed || len(b2) != len(b)
1185 }
1186 t.Logf("attempt %d length: %d. padding: %d.", i, len(b2), len(b2)-len(refBody))
1187 if len(b2) <= len(refBody) {
1188 t.Errorf("no padding applied,")
1189 }
1190
1191
1192
1193 b = b2
1194 }
1195 if !changed {
1196 t.Errorf("no change after 9 attempts")
1197 }
1198 }
1199
1200 func TestRandomJitterParanoid(t *testing.T) {
1201 r := httptest.NewRequest("GET", "/", nil)
1202 r.Header.Set("Accept-Encoding", "gzip")
1203
1204
1205 rng := rand.New(rand.NewSource(0))
1206 payload := make([]byte, 4096)
1207 _, err := io.ReadFull(rng, payload)
1208 if err != nil {
1209 t.Fatal(err)
1210 }
1211
1212 wrapper, err := NewWrapper(RandomJitter(256, 1024, true), MinSize(10))
1213 if err != nil {
1214 t.Fatal(err)
1215 }
1216 writePayload := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1217 w.Write(payload)
1218 })
1219 referenceHandler := GzipHandler(writePayload)
1220 w := httptest.NewRecorder()
1221 referenceHandler.ServeHTTP(w, r)
1222 result := w.Result()
1223 refBody, err := io.ReadAll(result.Body)
1224 if err != nil {
1225 t.Fatal(err)
1226 }
1227 t.Logf("Unmodified length: %d", len(refBody))
1228
1229 handler := wrapper(writePayload)
1230 w = httptest.NewRecorder()
1231 handler.ServeHTTP(w, r)
1232
1233 result = w.Result()
1234 b, err := io.ReadAll(result.Body)
1235 if err != nil {
1236 t.Fatal(err)
1237 }
1238
1239 if len(refBody) == len(b) {
1240 t.Fatal("padding was not applied")
1241 }
1242
1243 if err != nil {
1244 t.Fatal(err)
1245 }
1246 changed := false
1247 for i := 0; i < 10; i++ {
1248 w = httptest.NewRecorder()
1249 handler.ServeHTTP(w, r)
1250 result = w.Result()
1251 b2, err := io.ReadAll(result.Body)
1252 if err != nil {
1253 t.Fatal(err)
1254 }
1255 changed = changed || len(b2) != len(b)
1256 t.Logf("attempt %d length: %d. padding: %d.", i, len(b2), len(b2)-len(refBody))
1257 if len(b2) <= len(refBody) {
1258 t.Errorf("no padding applied,")
1259 }
1260 if i == 0 && changed {
1261 t.Error("length changed without payload change", len(b), "->", len(b2))
1262 }
1263
1264 payload[0]++
1265 b = b2
1266 }
1267 if !changed {
1268 t.Errorf("no change after 9 attempts")
1269 }
1270
1271
1272 handler = wrapper(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1273 for i := range payload {
1274 w.Write([]byte{payload[i]})
1275 }
1276 }))
1277
1278 for i := 0; i < 10; i++ {
1279 w = httptest.NewRecorder()
1280 handler.ServeHTTP(w, r)
1281 result = w.Result()
1282 b2, err := io.ReadAll(result.Body)
1283 if err != nil {
1284 t.Fatal(err)
1285 }
1286
1287 t.Logf("attempt %d length: %d. padding: %d.", i, len(b2), len(b2)-len(refBody))
1288 if len(b2) <= len(refBody) {
1289 t.Errorf("no padding applied,")
1290 }
1291 if i > 0 && len(b2) != len(b) {
1292 t.Error("length changed without payload change", len(b), "->", len(b2))
1293 }
1294
1295 payload[2048]++
1296 b = b2
1297 }
1298
1299
1300 handler = wrapper(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1301 w.Write(payload[:512])
1302 }))
1303 changed = false
1304 for i := 0; i < 10; i++ {
1305 w = httptest.NewRecorder()
1306 handler.ServeHTTP(w, r)
1307 result = w.Result()
1308 b2, err := io.ReadAll(result.Body)
1309 if err != nil {
1310 t.Fatal(err)
1311 }
1312 if i > 0 {
1313 changed = changed || len(b2) != len(b)
1314 }
1315 t.Logf("attempt %d length: %d. padding: %d.", i, len(b2), len(b2)-512)
1316 if len(b2) <= 512 {
1317 t.Errorf("no padding applied,")
1318 }
1319
1320 payload[500]++
1321 b = b2
1322 }
1323 if !changed {
1324 t.Errorf("no change after 9 attempts")
1325 }
1326
1327
1328
1329 handler = wrapper(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1330 w.Write(payload[:256])
1331 w.(http.Flusher).Flush()
1332 w.Write(payload[256:512])
1333 }))
1334
1335 changed = false
1336 for i := 0; i < 10; i++ {
1337 w = httptest.NewRecorder()
1338 handler.ServeHTTP(w, r)
1339 result = w.Result()
1340 b2, err := io.ReadAll(result.Body)
1341 if err != nil {
1342 t.Fatal(err)
1343 }
1344 if i > 0 {
1345 changed = changed || len(b2) != len(b)
1346 }
1347 t.Logf("attempt %d length: %d. padding: %d.", i, len(b2), len(b2)-512)
1348 if len(b2) <= 512 {
1349 t.Errorf("no padding applied,")
1350 }
1351
1352 payload[200]++
1353 b = b2
1354 }
1355 if !changed {
1356 t.Errorf("no change after 9 attempts")
1357 }
1358
1359
1360
1361 for i := 0; i < 10; i++ {
1362 w = httptest.NewRecorder()
1363 handler.ServeHTTP(w, r)
1364 result = w.Result()
1365 b2, err := io.ReadAll(result.Body)
1366 if err != nil {
1367 t.Fatal(err)
1368 }
1369 if i > 0 {
1370 changed = len(b2) != len(b)
1371 if changed {
1372 t.Errorf("mutating after flush seems to have affected output")
1373 }
1374 }
1375 t.Logf("attempt %d length: %d. padding: %d.", i, len(b2), len(b2)-512)
1376 if len(b2) <= 512 {
1377 t.Errorf("no padding applied,")
1378 }
1379
1380 payload[400]++
1381 b = b2
1382 }
1383
1384
1385 wrapper, err = NewWrapper(RandomJitter(256, -1, true), MinSize(10))
1386 if err != nil {
1387 t.Fatal(err)
1388 }
1389 handler = wrapper(writePayload)
1390 changed = false
1391 for i := 0; i < 10; i++ {
1392 w = httptest.NewRecorder()
1393 handler.ServeHTTP(w, r)
1394 result = w.Result()
1395 b2, err := io.ReadAll(result.Body)
1396 if err != nil {
1397 t.Fatal(err)
1398 }
1399 if i > 0 {
1400 changed = changed || len(b2) != len(b)
1401 }
1402 t.Logf("attempt %d length: %d. padding: %d.", i, len(b2), len(b2)-len(refBody))
1403 if len(b2) <= len(refBody) {
1404 t.Errorf("no padding applied,")
1405 }
1406
1407
1408
1409 b = b2
1410 }
1411 if !changed {
1412 t.Errorf("no change after 9 attempts")
1413 }
1414 }
1415
1416 var contentTypeTest2 = []struct {
1417 name string
1418 contentType string
1419 expectedGzip bool
1420 }{
1421 {
1422 name: "Always gzip when content types are empty",
1423 contentType: "",
1424 expectedGzip: true,
1425 },
1426 {
1427 name: "MIME match",
1428 contentType: "application/json",
1429 expectedGzip: true,
1430 },
1431 {
1432 name: "MIME no match",
1433 contentType: "text/xml",
1434 expectedGzip: true,
1435 },
1436
1437 {
1438 name: "MIME match case insensitive",
1439 contentType: "Video/Something",
1440 expectedGzip: false,
1441 },
1442 {
1443 name: "MIME match case insensitive",
1444 contentType: "audio/Something",
1445 expectedGzip: false,
1446 },
1447 {
1448 name: "MIME match ignore whitespace",
1449 contentType: " video/mp4",
1450 expectedGzip: false,
1451 },
1452 {
1453 name: "without prefix..",
1454 contentType: "avideo/mp4",
1455 expectedGzip: true,
1456 },
1457 {
1458 name: "application/zip",
1459 contentType: "application/zip;lalala",
1460 expectedGzip: false,
1461 },
1462 {
1463 name: "x-zip-compressed",
1464 contentType: "application/x-zip-compressed",
1465 expectedGzip: false,
1466 },
1467 {
1468 name: "application/x-gzip",
1469 contentType: "application/x-gzip",
1470 expectedGzip: false,
1471 },
1472 }
1473
1474 func TestDefaultContentTypes(t *testing.T) {
1475 for _, tt := range contentTypeTest2 {
1476 t.Run(tt.name, func(t *testing.T) {
1477 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1478 w.WriteHeader(http.StatusOK)
1479 w.Header().Set("Content-Type", tt.contentType)
1480 w.Write(testBody)
1481 })
1482
1483 wrapper, err := NewWrapper()
1484 assertNil(t, err)
1485
1486 req, _ := http.NewRequest("GET", "/whatever", nil)
1487 req.Header.Set("Accept-Encoding", "gzip")
1488 resp := httptest.NewRecorder()
1489 wrapper(handler).ServeHTTP(resp, req)
1490 res := resp.Result()
1491
1492 assertEqual(t, 200, res.StatusCode)
1493 if tt.expectedGzip {
1494 assertEqual(t, "gzip", res.Header.Get("Content-Encoding"))
1495 } else {
1496 assertNotEqual(t, "gzip", res.Header.Get("Content-Encoding"))
1497 }
1498 })
1499 }
1500 }
1501
1502 var sniffTests = []struct {
1503 desc string
1504 data []byte
1505 contentType string
1506 }{
1507
1508 {"Empty", []byte{}, "text/plain; charset=utf-8"},
1509 {"Binary", []byte{1, 2, 3}, "application/octet-stream"},
1510
1511 {"HTML document #1", []byte(`<HtMl><bOdY>blah blah blah</body></html>`), "text/html; charset=utf-8"},
1512 {"HTML document #2", []byte(`<HTML></HTML>`), "text/html; charset=utf-8"},
1513 {"HTML document #3 (leading whitespace)", []byte(` <!DOCTYPE HTML>...`), "text/html; charset=utf-8"},
1514 {"HTML document #4 (leading CRLF)", []byte("\r\n<html>..."), "text/html; charset=utf-8"},
1515
1516 {"Plain text", []byte(`This is not HTML. It has ☃ though.`), "text/plain; charset=utf-8"},
1517
1518 {"XML", []byte("\n<?xml!"), "text/xml; charset=utf-8"},
1519
1520
1521 {"Windows icon", []byte("\x00\x00\x01\x00"), "image/x-icon"},
1522 {"Windows cursor", []byte("\x00\x00\x02\x00"), "image/x-icon"},
1523 {"BMP image", []byte("BM..."), "image/bmp"},
1524 {"GIF 87a", []byte(`GIF87a`), "image/gif"},
1525 {"GIF 89a", []byte(`GIF89a...`), "image/gif"},
1526 {"WEBP image", []byte("RIFF\x00\x00\x00\x00WEBPVP"), "image/webp"},
1527 {"PNG image", []byte("\x89PNG\x0D\x0A\x1A\x0A"), "image/png"},
1528 {"JPEG image", []byte("\xFF\xD8\xFF"), "image/jpeg"},
1529
1530
1531 {"MIDI audio", []byte("MThd\x00\x00\x00\x06\x00\x01"), "audio/midi"},
1532 {"MP3 audio/MPEG audio", []byte("ID3\x03\x00\x00\x00\x00\x0f"), "audio/mpeg"},
1533 {"WAV audio #1", []byte("RIFFb\xb8\x00\x00WAVEfmt \x12\x00\x00\x00\x06"), "audio/wave"},
1534 {"WAV audio #2", []byte("RIFF,\x00\x00\x00WAVEfmt \x12\x00\x00\x00\x06"), "audio/wave"},
1535 {"AIFF audio #1", []byte("FORM\x00\x00\x00\x00AIFFCOMM\x00\x00\x00\x12\x00\x01\x00\x00\x57\x55\x00\x10\x40\x0d\xf3\x34"), "audio/aiff"},
1536
1537 {"OGG audio", []byte("OggS\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x7e\x46\x00\x00\x00\x00\x00\x00\x1f\xf6\xb4\xfc\x01\x1e\x01\x76\x6f\x72"), "application/ogg"},
1538 {"Must not match OGG", []byte("owow\x00"), "application/octet-stream"},
1539 {"Must not match OGG", []byte("oooS\x00"), "application/octet-stream"},
1540 {"Must not match OGG", []byte("oggS\x00"), "application/octet-stream"},
1541
1542
1543 {"MP4 video", []byte("\x00\x00\x00\x18ftypmp42\x00\x00\x00\x00mp42isom<\x06t\xbfmdat"), "video/mp4"},
1544 {"AVI video #1", []byte("RIFF,O\n\x00AVI LISTÀ"), "video/avi"},
1545 {"AVI video #2", []byte("RIFF,\n\x00\x00AVI LISTÀ"), "video/avi"},
1546
1547
1548
1549 {"TTF sample I", []byte("\x00\x01\x00\x00\x00\x17\x01\x00\x00\x04\x01\x60\x4f"), "font/ttf"},
1550 {"TTF sample II", []byte("\x00\x01\x00\x00\x00\x0e\x00\x80\x00\x03\x00\x60\x46"), "font/ttf"},
1551
1552 {"OTTO sample I", []byte("\x4f\x54\x54\x4f\x00\x0e\x00\x80\x00\x03\x00\x60\x42\x41\x53\x45"), "font/otf"},
1553
1554 {"woff sample I", []byte("\x77\x4f\x46\x46\x00\x01\x00\x00\x00\x00\x30\x54\x00\x0d\x00\x00"), "font/woff"},
1555 {"woff2 sample", []byte("\x77\x4f\x46\x32\x00\x01\x00\x00\x00"), "font/woff2"},
1556 {"wasm sample", []byte("\x00\x61\x73\x6d\x01\x00"), "application/wasm"},
1557
1558
1559 {"RAR v1.5-v4.0", []byte("Rar!\x1A\x07\x00"), "application/x-rar-compressed"},
1560 {"RAR v5+", []byte("Rar!\x1A\x07\x01\x00"), "application/x-rar-compressed"},
1561 {"Incorrect RAR v1.5-v4.0", []byte("Rar \x1A\x07\x00"), "application/octet-stream"},
1562 {"Incorrect RAR v5+", []byte("Rar \x1A\x07\x01\x00"), "application/octet-stream"},
1563 }
1564
1565 func TestContentTypeDetect(t *testing.T) {
1566 for _, tt := range sniffTests {
1567 t.Run(tt.desc, func(t *testing.T) {
1568 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1569 w.WriteHeader(http.StatusOK)
1570 for i := range tt.data {
1571
1572 w.Write([]byte{tt.data[i]})
1573 }
1574 w.Write(testBody)
1575 })
1576
1577 wrapper, err := NewWrapper()
1578 assertNil(t, err)
1579
1580 req, _ := http.NewRequest("GET", "/whatever", nil)
1581 req.Header.Set("Accept-Encoding", "gzip")
1582 resp := httptest.NewRecorder()
1583 wrapper(handler).ServeHTTP(resp, req)
1584 res := resp.Result()
1585
1586 assertEqual(t, 200, res.StatusCode)
1587 assertEqual(t, tt.contentType, res.Header.Get("Content-Type"))
1588 shouldGZ := DefaultContentTypeFilter(tt.contentType)
1589 if shouldGZ {
1590 assertEqual(t, "gzip", res.Header.Get("Content-Encoding"))
1591 } else {
1592 assertNotEqual(t, "gzip", res.Header.Get("Content-Encoding"))
1593 }
1594 })
1595 t.Run(tt.desc+"empty", func(t *testing.T) {
1596 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1597 w.Header().Set("Content-Type", "")
1598 w.WriteHeader(http.StatusOK)
1599 for i := range tt.data {
1600
1601 w.Write([]byte{tt.data[i]})
1602 }
1603 w.Write(testBody)
1604 })
1605
1606 wrapper, err := NewWrapper()
1607 assertNil(t, err)
1608
1609 req, _ := http.NewRequest("GET", "/whatever", nil)
1610 req.Header.Set("Accept-Encoding", "gzip")
1611 resp := httptest.NewRecorder()
1612 wrapper(handler).ServeHTTP(resp, req)
1613 res := resp.Result()
1614
1615 assertEqual(t, 200, res.StatusCode)
1616
1617 assertEqual(t, "", res.Header.Get("Content-Type"))
1618 shouldGZ := DefaultContentTypeFilter(tt.contentType)
1619 if shouldGZ {
1620 assertEqual(t, "gzip", res.Header.Get("Content-Encoding"))
1621 } else {
1622 assertNotEqual(t, "gzip", res.Header.Get("Content-Encoding"))
1623 }
1624 })
1625 t.Run(tt.desc+"flush", func(t *testing.T) {
1626 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1627 w.Header().Set("Content-Type", "")
1628 w.WriteHeader(http.StatusOK)
1629 for i := range tt.data {
1630
1631 w.Write([]byte{tt.data[i]})
1632 }
1633 w.(http.Flusher).Flush()
1634 w.Write(testBody)
1635 })
1636
1637 wrapper, err := NewWrapper()
1638 assertNil(t, err)
1639
1640 req, _ := http.NewRequest("GET", "/whatever", nil)
1641 req.Header.Set("Accept-Encoding", "gzip")
1642 resp := httptest.NewRecorder()
1643 wrapper(handler).ServeHTTP(resp, req)
1644 res := resp.Result()
1645
1646 assertEqual(t, 200, res.StatusCode)
1647
1648 assertEqual(t, "", res.Header.Get("Content-Type"))
1649 shouldGZ := DefaultContentTypeFilter(tt.contentType)
1650 if shouldGZ {
1651 assertEqual(t, "gzip", res.Header.Get("Content-Encoding"))
1652 } else {
1653 assertNotEqual(t, "gzip", res.Header.Get("Content-Encoding"))
1654 }
1655 })
1656 }
1657 }
1658
1659
1660
1661 func BenchmarkGzipHandler_S2k(b *testing.B) {
1662 benchmark(b, false, 2048, CompressionLevel(gzip.DefaultCompression))
1663 }
1664 func BenchmarkGzipHandler_S20k(b *testing.B) {
1665 benchmark(b, false, 20480, CompressionLevel(gzip.DefaultCompression))
1666 }
1667 func BenchmarkGzipHandler_S100k(b *testing.B) {
1668 benchmark(b, false, 102400, CompressionLevel(gzip.DefaultCompression))
1669 }
1670 func BenchmarkGzipHandler_P2k(b *testing.B) {
1671 benchmark(b, true, 2048, CompressionLevel(gzip.DefaultCompression))
1672 }
1673 func BenchmarkGzipHandler_P20k(b *testing.B) {
1674 benchmark(b, true, 20480, CompressionLevel(gzip.DefaultCompression))
1675 }
1676 func BenchmarkGzipHandler_P100k(b *testing.B) {
1677 benchmark(b, true, 102400, CompressionLevel(gzip.DefaultCompression))
1678 }
1679
1680 func BenchmarkGzipBestSpeedHandler_S2k(b *testing.B) {
1681 benchmark(b, false, 2048, CompressionLevel(gzip.BestSpeed))
1682 }
1683 func BenchmarkGzipBestSpeedHandler_S20k(b *testing.B) {
1684 benchmark(b, false, 20480, CompressionLevel(gzip.BestSpeed))
1685 }
1686 func BenchmarkGzipBestSpeedHandler_S100k(b *testing.B) {
1687 benchmark(b, false, 102400, CompressionLevel(gzip.BestSpeed))
1688 }
1689 func BenchmarkGzipBestSpeedHandler_P2k(b *testing.B) {
1690 benchmark(b, true, 2048, CompressionLevel(gzip.BestSpeed))
1691 }
1692 func BenchmarkGzipBestSpeedHandler_P20k(b *testing.B) {
1693 benchmark(b, true, 20480, CompressionLevel(gzip.BestSpeed))
1694 }
1695 func BenchmarkGzipBestSpeedHandler_P100k(b *testing.B) {
1696 benchmark(b, true, 102400, CompressionLevel(gzip.BestSpeed))
1697 }
1698
1699 func Benchmark2kJitter(b *testing.B) {
1700 benchmark(b, false, 2048, CompressionLevel(gzip.BestSpeed), RandomJitter(32, 0, false))
1701 }
1702
1703 func Benchmark2kJitterParanoid(b *testing.B) {
1704 benchmark(b, false, 2048, CompressionLevel(gzip.BestSpeed), RandomJitter(32, 0, true))
1705 }
1706
1707 func Benchmark2kJitterRNG(b *testing.B) {
1708 benchmark(b, false, 2048, CompressionLevel(gzip.BestSpeed), RandomJitter(32, -1, false))
1709 }
1710
1711
1712
1713 func gzipStrLevel(s []byte, lvl int) []byte {
1714 var b bytes.Buffer
1715 w, _ := gzip.NewWriterLevel(&b, lvl)
1716 w.Write(s)
1717 w.Close()
1718 return b.Bytes()
1719 }
1720
1721 func benchmark(b *testing.B, parallel bool, size int, opts ...option) {
1722 bin, err := os.ReadFile("testdata/benchmark.json")
1723 if err != nil {
1724 b.Fatal(err)
1725 }
1726
1727 req, _ := http.NewRequest("GET", "/whatever", nil)
1728 req.Header.Set("Accept-Encoding", "gzip")
1729 handler := newTestHandlerLevel(bin[:size], opts...)
1730
1731 b.ReportAllocs()
1732 b.SetBytes(int64(size))
1733 if parallel {
1734 b.ResetTimer()
1735 b.RunParallel(func(pb *testing.PB) {
1736 for pb.Next() {
1737 runBenchmark(b, req, handler)
1738 }
1739 })
1740 } else {
1741 b.ResetTimer()
1742 for i := 0; i < b.N; i++ {
1743 runBenchmark(b, req, handler)
1744 }
1745 }
1746 }
1747
1748 func runBenchmark(b *testing.B, req *http.Request, handler http.Handler) {
1749 res := httptest.NewRecorder()
1750 handler.ServeHTTP(res, req)
1751 if code := res.Code; code != 200 {
1752 b.Fatalf("Expected 200 but got %d", code)
1753 } else if blen := res.Body.Len(); blen < 500 {
1754 b.Fatalf("Expected complete response body, but got %d bytes", blen)
1755 }
1756 }
1757
1758 func newTestHandler(body []byte) http.Handler {
1759 return GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1760 switch r.URL.Path {
1761 case "/gzipped":
1762 w.Header().Set("Content-Encoding", "gzip")
1763 w.Write(body)
1764 case "/zstd":
1765 w.Header().Set("Content-Encoding", "zstd")
1766 w.Write(body)
1767 default:
1768 w.Write(body)
1769 }
1770 }))
1771 }
1772
1773 func newTestHandlerLevel(body []byte, opts ...option) http.Handler {
1774 wrapper, err := NewWrapper(opts...)
1775 if err != nil {
1776 panic(err)
1777 }
1778 return wrapper(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1779 switch r.URL.Path {
1780 case "/gzipped":
1781 w.Header().Set("Content-Encoding", "gzip")
1782 w.Write(body)
1783 default:
1784 w.Write(body)
1785 }
1786 }))
1787 }
1788
1789 func TestGzipHandlerNilContentType(t *testing.T) {
1790
1791 handler := newTestHandler(testBody)
1792
1793
1794
1795 req, _ := http.NewRequest("GET", "/whatever", nil)
1796 req.Header.Set("Accept-Encoding", "gzip")
1797 res := httptest.NewRecorder()
1798 res.Header()["Content-Type"] = nil
1799 handler.ServeHTTP(res, req)
1800
1801 assertEqual(t, "", res.Header().Get("Content-Type"))
1802 }
1803
1804
1805 func Test1xxResponses(t *testing.T) {
1806
1807 if !shouldWrite1xxResponses() {
1808 return
1809 }
1810
1811 wrapper, _ := NewWrapper()
1812 handler := wrapper(http.HandlerFunc(
1813 func(w http.ResponseWriter, r *http.Request) {
1814 h := w.Header()
1815 h.Add("Link", "</style.css>; rel=preload; as=style")
1816 h.Add("Link", "</script.js>; rel=preload; as=script")
1817 w.WriteHeader(http.StatusEarlyHints)
1818
1819 h.Add("Link", "</foo.js>; rel=preload; as=script")
1820 w.WriteHeader(http.StatusProcessing)
1821
1822 w.Write(testBody)
1823 },
1824 ))
1825
1826 frontend := httptest.NewServer(handler)
1827 defer frontend.Close()
1828 frontendClient := frontend.Client()
1829
1830 checkLinkHeaders := func(t *testing.T, expected, got []string) {
1831 t.Helper()
1832
1833 if len(expected) != len(got) {
1834 t.Errorf("Expected %d link headers; got %d", len(expected), len(got))
1835 }
1836
1837 for i := range expected {
1838 if i >= len(got) {
1839 t.Errorf("Expected %q link header; got nothing", expected[i])
1840
1841 continue
1842 }
1843
1844 if expected[i] != got[i] {
1845 t.Errorf("Expected %q link header; got %q", expected[i], got[i])
1846 }
1847 }
1848 }
1849
1850 var respCounter uint8
1851 trace := &httptrace.ClientTrace{
1852 Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
1853 switch code {
1854 case http.StatusEarlyHints:
1855 checkLinkHeaders(t, []string{"</style.css>; rel=preload; as=style", "</script.js>; rel=preload; as=script"}, header["Link"])
1856 case http.StatusProcessing:
1857 checkLinkHeaders(t, []string{"</style.css>; rel=preload; as=style", "</script.js>; rel=preload; as=script", "</foo.js>; rel=preload; as=script"}, header["Link"])
1858 default:
1859 t.Error("Unexpected 1xx response")
1860 }
1861
1862 respCounter++
1863
1864 return nil
1865 },
1866 }
1867 req, _ := http.NewRequestWithContext(httptrace.WithClientTrace(context.Background(), trace), "GET", frontend.URL, nil)
1868 req.Header.Set("Accept-Encoding", "gzip")
1869
1870 res, err := frontendClient.Do(req)
1871 if err != nil {
1872 t.Fatalf("Get: %v", err)
1873 }
1874
1875 defer res.Body.Close()
1876
1877 if respCounter != 2 {
1878 t.Errorf("Expected 2 1xx responses; got %d", respCounter)
1879 }
1880 checkLinkHeaders(t, []string{"</style.css>; rel=preload; as=style", "</script.js>; rel=preload; as=script", "</foo.js>; rel=preload; as=script"}, res.Header["Link"])
1881
1882 assertEqual(t, "gzip", res.Header.Get("Content-Encoding"))
1883
1884 body, _ := io.ReadAll(res.Body)
1885 assertEqual(t, gzipStrLevel(testBody, gzip.DefaultCompression), body)
1886 }
1887
1888 func TestContentTypeDetectWithJitter(t *testing.T) {
1889 t.Parallel()
1890 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1891 content := `<!DOCTYPE html>` + strings.Repeat("foo", 400)
1892 w.Write([]byte(content))
1893 })
1894
1895 for _, tc := range []struct {
1896 name string
1897 wrapper func(http.Handler) (http.Handler, error)
1898 }{
1899 {
1900 name: "no wrapping",
1901 wrapper: func(h http.Handler) (http.Handler, error) {
1902 return h, nil
1903 },
1904 },
1905 {
1906 name: "default",
1907 wrapper: func(h http.Handler) (http.Handler, error) {
1908 wrapper, err := NewWrapper()
1909 if err != nil {
1910 return nil, err
1911 }
1912 return wrapper(h), nil
1913 },
1914 },
1915 {
1916 name: "jitter, default buffer",
1917 wrapper: func(h http.Handler) (http.Handler, error) {
1918 wrapper, err := NewWrapper(RandomJitter(32, 0, false))
1919 if err != nil {
1920 return nil, err
1921 }
1922 return wrapper(h), nil
1923 },
1924 },
1925 {
1926 name: "jitter, small buffer",
1927 wrapper: func(h http.Handler) (http.Handler, error) {
1928 wrapper, err := NewWrapper(RandomJitter(32, DefaultMinSize, false))
1929 if err != nil {
1930 return nil, err
1931 }
1932 return wrapper(h), nil
1933 },
1934 },
1935 } {
1936 tc := tc
1937 t.Run(tc.name, func(t *testing.T) {
1938 t.Parallel()
1939
1940 handler, err := tc.wrapper(handler)
1941 assertNil(t, err)
1942
1943 req, resp := httptest.NewRequest(http.MethodGet, "/", nil), httptest.NewRecorder()
1944 req.Header.Add("Accept-Encoding", "gzip")
1945
1946 handler.ServeHTTP(resp, req)
1947
1948 assertEqual(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
1949 })
1950 }
1951 }
1952
View as plain text