diff --git a/router-tests/response_compression_test.go b/router-tests/response_compression_test.go index 131d6cbf3e..4390bbcdeb 100644 --- a/router-tests/response_compression_test.go +++ b/router-tests/response_compression_test.go @@ -2,14 +2,14 @@ package integration import ( "bytes" - "compress/flate" "compress/gzip" "encoding/json" + "github.com/buger/jsonparser" "io" "net/http" + "strings" "testing" - "github.com/andybalholm/brotli" "github.com/stretchr/testify/require" "github.com/wundergraph/cosmo/router-tests/testenv" ) @@ -25,21 +25,6 @@ func decompressGzip(t *testing.T, body io.Reader) []byte { return data } -func decompressDeflate(t *testing.T, body io.Reader) []byte { - dr := flate.NewReader(body) - defer dr.Close() - data, err := io.ReadAll(dr) - require.NoError(t, err) - return data -} - -func decompressBrotli(t *testing.T, body io.Reader) []byte { - br := brotli.NewReader(body) - data, err := io.ReadAll(br) - require.NoError(t, err) - return data -} - func decompressNone(t *testing.T, body io.Reader) []byte { data, err := io.ReadAll(body) require.NoError(t, err) @@ -49,24 +34,41 @@ func decompressNone(t *testing.T, body io.Reader) []byte { func TestResponseCompression(t *testing.T) { t.Parallel() + employeesIdDataMinSizeGzip := `{"data":{"employees":[{"id":1}` + strings.Repeat(`,{"id":1}`, 200) + `]}}` + testCases := []struct { name string encoding string decompressFunc func(t *testing.T, body io.Reader) []byte expectEncoding bool + responseData string }{ - {"gzip", "gzip", decompressGzip, true}, - {"deflate", "deflate", decompressDeflate, true}, - {"brotli", "br", decompressBrotli, true}, - {"identity", "identity", decompressNone, false}, // NO Encoding - {"zstd", "zstd", decompressNone, false}, // Unsuported Encoding + {"gzip with min size", "gzip", decompressGzip, true, employeesIdDataMinSizeGzip}, // Gzip Encoding with min size + {"no gzip because request is too small", "", decompressGzip, false, employeesIdData}, // No Gzip Encoding because of min size + {"identity", "identity", decompressNone, false, employeesIdData}, // NO Encoding + {"zstd", "zstd", decompressNone, false, employeesIdData}, // Unsuported Encoding } for _, tc := range testCases { tc := tc // capture range variable t.Run(tc.name, func(t *testing.T) { t.Parallel() // mark the subtest as parallel - testenv.Run(t, &testenv.Config{}, func(t *testing.T, xEnv *testenv.Environment) { + testenv.Run(t, &testenv.Config{ + Subgraphs: testenv.SubgraphsConfig{ + Employees: testenv.SubgraphConfig{ + Middleware: func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + data, err := io.ReadAll(r.Body) + require.NoError(t, err) + _, dt, _, _ := jsonparser.Get(data, "extensions", "persistedQuery") + require.Equal(t, jsonparser.NotExist, dt) + _, _ = w.Write([]byte(tc.responseData)) + }) + }, + }, + }, + }, func(t *testing.T, xEnv *testenv.Environment) { headers := http.Header{ "Content-Type": []string{"application/json"}, } @@ -87,13 +89,12 @@ func TestResponseCompression(t *testing.T) { if tc.expectEncoding { require.Equal(t, tc.encoding, res.Header.Get("Content-Encoding")) + decompressedBody := tc.decompressFunc(t, res.Body) + require.JSONEq(t, tc.responseData, string(decompressedBody)) } else { require.Empty(t, res.Header.Get("Content-Encoding")) } require.Contains(t, res.Header.Get("Content-Type"), "application/json") - - decompressedBody := tc.decompressFunc(t, res.Body) - require.JSONEq(t, employeesIdData, string(decompressedBody)) }) }) } diff --git a/router/core/graph_server.go b/router/core/graph_server.go index 2759bbdaab..eaf4ee3f63 100644 --- a/router/core/graph_server.go +++ b/router/core/graph_server.go @@ -5,13 +5,13 @@ import ( "crypto/ecdsa" "errors" "fmt" - "io" + "github.com/klauspost/compress/gzhttp" + "github.com/klauspost/compress/gzip" "net/http" "net/url" "strings" "time" - br "github.com/andybalholm/brotli" "github.com/cloudflare/backoff" "github.com/dgraph-io/ristretto" "github.com/go-chi/chi/v5" @@ -200,19 +200,24 @@ func newGraphServer(ctx context.Context, r *Router, routerConfig *nodev1.RouterC return nil, fmt.Errorf("failed to build feature flag handler: %w", err) } - brCompressor := middleware.NewCompressor(5, CustomCompressibleContentTypes...) - brCompressor.SetEncoder("br", func(w io.Writer, level int) io.Writer { - return br.NewWriterLevel(w, level) - }) + wrapper, err := gzhttp.NewWrapper( + gzhttp.MinSize(1024), // 1KB + gzhttp.CompressionLevel(gzip.DefaultCompression), + gzhttp.ContentTypes(CompressibleContentTypes), + ) + if err != nil { + return nil, fmt.Errorf("failed to create gzip wrapper: %w", err) + } /** * A group where we can selectively apply middlewares to the graphql endpoint */ httpRouter.Group(func(cr chi.Router) { - // We are applying it conditionally because brotli compressing the 3MB playground is very slow - cr.Use(middleware.Compress(5, CustomCompressibleContentTypes...)) - cr.Use(brCompressor.Handler) + // We are applying it conditionally because compressing 3MB playground is still slow even with stdlib gzip + cr.Use(func(h http.Handler) http.Handler { + return wrapper(h) + }) // Mount the feature flag handler. It calls the base mux if no feature flag is set. cr.Mount(r.graphqlPath, multiGraphHandler) diff --git a/router/core/router.go b/router/core/router.go index 56cff0fa9d..035c1205c5 100644 --- a/router/core/router.go +++ b/router/core/router.go @@ -51,7 +51,7 @@ const ( Redact IPAnonymizationMethod = "redact" ) -var CustomCompressibleContentTypes = []string{ +var CompressibleContentTypes = []string{ "text/html", "text/css", "text/plain", @@ -63,6 +63,8 @@ var CustomCompressibleContentTypes = []string{ "application/rss+xml", "image/svg+xml", "application/graphql", + "application/graphql-response+json", + "application/graphql+json", } type ( diff --git a/router/go.mod b/router/go.mod index 21fe814de6..1298862573 100644 --- a/router/go.mod +++ b/router/go.mod @@ -8,11 +8,13 @@ require ( connectrpc.com/connect v1.16.2 github.com/MicahParks/keyfunc/v2 v2.1.0 github.com/alitto/pond v1.8.3 - github.com/andybalholm/brotli v1.1.0 + github.com/andybalholm/brotli v1.1.0 // indirect github.com/buger/jsonparser v1.1.1 github.com/cespare/xxhash/v2 v2.2.0 github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a - github.com/dgraph-io/ristretto v0.1.1 + // References to main that includes the fix for the race with ristretto.Close() + // Link: https://github.com/dgraph-io/ristretto/pull/384 + github.com/dgraph-io/ristretto v0.1.2-0.20240723054643-f5997484152c github.com/dustin/go-humanize v1.0.1 github.com/go-chi/chi/v5 v5.0.11 github.com/go-redis/redis_rate/v10 v10.0.1 @@ -65,6 +67,7 @@ require ( ) require ( + github.com/klauspost/compress v1.17.9 github.com/valyala/fastjson v1.6.4 golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 ) @@ -89,7 +92,6 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/jensneuse/byte-template v0.0.0-20200214152254-4f3cf06e5c68 // indirect github.com/kingledion/go-tools v0.6.0 // indirect - github.com/klauspost/compress v1.17.8 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -126,7 +128,4 @@ require ( nhooyr.io/websocket v1.8.11 // indirect ) -// To fix race with ristretto.Close() https://github.com/dgraph-io/ristretto/pull/384 -replace github.com/dgraph-io/ristretto v0.1.1 => github.com/wundergraph/ristretto v0.0.0-20240715072905-d30a6481d4bf - //replace github.com/wundergraph/graphql-go-tools/v2 => ../../graphql-go-tools/v2 diff --git a/router/go.sum b/router/go.sum index e43ab70693..d27bff39cf 100644 --- a/router/go.sum +++ b/router/go.sum @@ -28,6 +28,8 @@ github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/ristretto v0.1.2-0.20240723054643-f5997484152c h1:V2+MhiAoTwUNENo9PFoz2NSr0VDJEzIARLuzb30YyqA= +github.com/dgraph-io/ristretto v0.1.2-0.20240723054643-f5997484152c/go.mod h1:swkazRqnUf1N62d0Nutz7KIj2UKqsm/H8tD0nBJAXqM= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= @@ -112,8 +114,8 @@ github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa github.com/kingledion/go-tools v0.6.0 h1:y8C/4mWoHgLkO45dB+Y/j0o4Y4WUB5lDTAcMPMtFpTg= github.com/kingledion/go-tools v0.6.0/go.mod h1:qcDJQxBui/H/hterGb90GMlLs9Yi7QrwaJL8OGdbsms= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= -github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=