Skip to content

Commit 6b05f45

Browse files
Move compression logic into its own middleware
1 parent b77cccb commit 6b05f45

File tree

3 files changed

+126
-31
lines changed

3 files changed

+126
-31
lines changed

internal/compression_handler.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package internal
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/klauspost/compress/gzhttp"
7+
)
8+
9+
func NewCompressionHandler(jitter int, disableOnAuth bool, next http.Handler) http.Handler {
10+
var wrapper func(http.Handler) http.HandlerFunc
11+
var err error
12+
13+
if jitter > 0 {
14+
wrapper, err = gzhttp.NewWrapper(
15+
gzhttp.MinSize(1024),
16+
gzhttp.CompressionLevel(6),
17+
gzhttp.RandomJitter(jitter, 0, false),
18+
)
19+
} else {
20+
wrapper, err = gzhttp.NewWrapper(
21+
gzhttp.MinSize(1024),
22+
gzhttp.CompressionLevel(6),
23+
)
24+
}
25+
26+
if err != nil {
27+
panic("failed to create gzip wrapper: " + err.Error())
28+
}
29+
30+
handler := wrapper(next)
31+
32+
if disableOnAuth {
33+
return NewCompressionGuardHandler(handler)
34+
}
35+
36+
return handler
37+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package internal
2+
3+
import (
4+
"compress/gzip"
5+
"io"
6+
"net/http"
7+
"net/http/httptest"
8+
"strings"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestCompressionHandler(t *testing.T) {
16+
largeBody := strings.Repeat("A", 2000)
17+
18+
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19+
w.Header().Set("Content-Type", "text/plain")
20+
_, err := w.Write([]byte(largeBody))
21+
require.NoError(t, err)
22+
})
23+
24+
t.Run("compresses responses", func(t *testing.T) {
25+
handler := NewCompressionHandler(0, false, upstream)
26+
27+
req := httptest.NewRequest("GET", "/", nil)
28+
req.Header.Set("Accept-Encoding", "gzip")
29+
rr := httptest.NewRecorder()
30+
31+
handler.ServeHTTP(rr, req)
32+
33+
assert.Equal(t, "gzip", rr.Header().Get("Content-Encoding"))
34+
35+
reader, err := gzip.NewReader(rr.Body)
36+
require.NoError(t, err)
37+
defer reader.Close()
38+
body, err := io.ReadAll(reader)
39+
require.NoError(t, err)
40+
assert.Equal(t, largeBody, string(body))
41+
})
42+
43+
t.Run("applies jitter when configured", func(t *testing.T) {
44+
handler := NewCompressionHandler(32, false, upstream)
45+
46+
req := httptest.NewRequest("GET", "/", nil)
47+
req.Header.Set("Accept-Encoding", "gzip")
48+
rr := httptest.NewRecorder()
49+
50+
handler.ServeHTTP(rr, req)
51+
52+
require.Equal(t, "gzip", rr.Header().Get("Content-Encoding"))
53+
54+
// Check for GZIP header with FCOMMENT flag (0x10)
55+
bodyBytes := rr.Body.Bytes()
56+
require.Greater(t, len(bodyBytes), 10)
57+
hasComment := (bodyBytes[3] & 0x10) != 0
58+
assert.True(t, hasComment, "Expected FCOMMENT flag due to jitter")
59+
})
60+
61+
t.Run("wraps with guard when disableOnAuth is true", func(t *testing.T) {
62+
handler := NewCompressionHandler(0, true, upstream)
63+
64+
req := httptest.NewRequest("GET", "/", nil)
65+
req.Header.Set("Accept-Encoding", "gzip")
66+
req.Header.Set("Cookie", "session=secret")
67+
rr := httptest.NewRecorder()
68+
69+
handler.ServeHTTP(rr, req)
70+
71+
// Should NOT be compressed due to Cookie header
72+
assert.Empty(t, rr.Header().Get("Content-Encoding"))
73+
assert.Equal(t, largeBody, rr.Body.String())
74+
})
75+
76+
t.Run("compresses authenticated requests when disableOnAuth is false", func(t *testing.T) {
77+
handler := NewCompressionHandler(0, false, upstream)
78+
79+
req := httptest.NewRequest("GET", "/", nil)
80+
req.Header.Set("Accept-Encoding", "gzip")
81+
req.Header.Set("Cookie", "session=secret")
82+
rr := httptest.NewRecorder()
83+
84+
handler.ServeHTTP(rr, req)
85+
86+
assert.Equal(t, "gzip", rr.Header().Get("Content-Encoding"))
87+
})
88+
}

internal/handler.go

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import (
44
"log/slog"
55
"net/http"
66
"net/url"
7-
8-
"github.com/klauspost/compress/gzhttp"
97
)
108

119
type HandlerOptions struct {
@@ -29,35 +27,7 @@ func NewHandler(options HandlerOptions) http.Handler {
2927
handler = NewRequestStartHandler(handler)
3028

3129
if options.gzipCompressionEnabled {
32-
var wrapper func(http.Handler) http.HandlerFunc
33-
var err error
34-
35-
if options.gzipCompressionJitter > 0 {
36-
wrapper, err = gzhttp.NewWrapper(
37-
gzhttp.MinSize(1024),
38-
gzhttp.CompressionLevel(6),
39-
gzhttp.RandomJitter(options.gzipCompressionJitter, 0, false),
40-
)
41-
} else {
42-
wrapper, err = gzhttp.NewWrapper(
43-
gzhttp.MinSize(1024),
44-
gzhttp.CompressionLevel(6),
45-
)
46-
}
47-
48-
if err != nil {
49-
// If we cannot create the wrapper with the requested configuration (including jitter),
50-
// we must fail hard rather than silently downgrading security or performance.
51-
panic("failed to create gzip wrapper: " + err.Error())
52-
}
53-
54-
gzipHandler := wrapper(handler)
55-
56-
if options.gzipCompressionDisableOnAuth {
57-
handler = NewCompressionGuardHandler(gzipHandler)
58-
} else {
59-
handler = gzipHandler
60-
}
30+
handler = NewCompressionHandler(options.gzipCompressionJitter, options.gzipCompressionDisableOnAuth, handler)
6131
}
6232

6333
if options.maxRequestBody > 0 {

0 commit comments

Comments
 (0)