diff --git a/demo/go.mod b/demo/go.mod index 818f86df5a..92853f18fe 100644 --- a/demo/go.mod +++ b/demo/go.mod @@ -13,9 +13,9 @@ require ( github.com/rs/cors v1.11.0 github.com/vektah/gqlparser/v2 v2.5.16 github.com/wundergraph/cosmo/composition-go v0.0.0-20240124120900-5effe48a4a1d - github.com/wundergraph/cosmo/router v0.0.0-20250106104112-39128d50b761 + github.com/wundergraph/cosmo/router v0.0.0-20250107115408-cdd3d47d6424 github.com/wundergraph/cosmo/router-tests v0.0.0-20241213115435-a249dba8c52a - github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.138 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.138.0.20250106145350-20e4f82cea6b go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 go.opentelemetry.io/otel v1.28.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1 @@ -27,7 +27,8 @@ require ( require ( connectrpc.com/connect v1.16.2 // indirect - github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect + github.com/MicahParks/jwkset v0.5.19 // indirect + github.com/MicahParks/keyfunc/v3 v3.3.5 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/benbjohnson/clock v1.3.0 // indirect @@ -61,7 +62,7 @@ require ( github.com/gobwas/ws v1.4.0 // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/goccy/go-yaml v1.13.4 // indirect - github.com/golang-jwt/jwt/v5 v5.2.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect @@ -121,7 +122,7 @@ require ( github.com/twmb/franz-go/pkg/kadm v1.11.0 // indirect github.com/twmb/franz-go/pkg/kmsg v1.7.0 // indirect github.com/urfave/cli/v2 v2.27.2 // indirect - github.com/wundergraph/astjson v0.0.0-20250102160438-534f686313e6 // indirect + github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 // indirect github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/contrib v1.16.1 // indirect @@ -143,7 +144,7 @@ require ( golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/mod v0.18.0 // indirect golang.org/x/net v0.33.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.22.0 // indirect diff --git a/demo/go.sum b/demo/go.sum index e78a53073f..52394df576 100644 --- a/demo/go.sum +++ b/demo/go.sum @@ -3,8 +3,10 @@ connectrpc.com/connect v1.16.2/go.mod h1:n2kgwskMHXC+lVqb18wngEpF95ldBHXjZYJussz github.com/99designs/gqlgen v0.17.49 h1:b3hNGexHd33fBSAd4NDT/c3NCcQzcAVkknhN9ym36YQ= github.com/99designs/gqlgen v0.17.49/go.mod h1:tC8YFVZMed81x7UJ7ORUwXF4Kn6SXuucFqQBhN8+BU0= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k= -github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= +github.com/MicahParks/jwkset v0.5.19 h1:XZCsgJv05DBCvxEHYEHlSafqiuVn5ESG0VRB331Fxhw= +github.com/MicahParks/jwkset v0.5.19/go.mod h1:q8ptTGn/Z9c4MwbcfeCDssADeVQb3Pk7PnVxrvi+2QY= +github.com/MicahParks/keyfunc/v3 v3.3.5 h1:7ceAJLUAldnoueHDNzF8Bx06oVcQ5CfJnYwNt1U3YYo= +github.com/MicahParks/keyfunc/v3 v3.3.5/go.mod h1:SdCCyMJn/bYqWDvARspC6nCT8Sk74MjuAY22C7dCST8= github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE= github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= @@ -108,8 +110,8 @@ github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.13.4 h1:XOnLX9GqT+kH/gB7YzCMUiDBFU9B7pm3HZz6kyeDPkk= github.com/goccy/go-yaml v1.13.4/go.mod h1:IjYwxUiJDoqpx2RmbdjMUceGHZwYLon3sfOGl5Hi9lc= -github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= -github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -299,16 +301,16 @@ github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0vy5p8= github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww= -github.com/wundergraph/astjson v0.0.0-20250102160438-534f686313e6 h1:VNJdQaqaHtc+02Bl3N9hw0b8sjCSh8ginYYcVKaTBfE= -github.com/wundergraph/astjson v0.0.0-20250102160438-534f686313e6/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= +github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTBjW+SZK4mhxTTBVpxcqeBgWF1Rfmltbfk= +github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= github.com/wundergraph/cosmo/composition-go v0.0.0-20240124120900-5effe48a4a1d h1:NEUrhuqOaTO1dpW8pz2tu6dKbQAqFvgiF/m4NXdzZm0= github.com/wundergraph/cosmo/composition-go v0.0.0-20240124120900-5effe48a4a1d/go.mod h1:9I3gPMAlAY+m1/cFL20iN7XHTyuZd3VT5ijccdU/FsI= -github.com/wundergraph/cosmo/router v0.0.0-20250106104112-39128d50b761 h1:3cjQnWzqgF57UJ4XsEQm4kodP2yRuIMUGmJrM+R/0c0= -github.com/wundergraph/cosmo/router v0.0.0-20250106104112-39128d50b761/go.mod h1:+0hvrL1XkMHiBcXgDWbr6boEB2LV3mPZWvDh/hfo9uM= +github.com/wundergraph/cosmo/router v0.0.0-20250107115408-cdd3d47d6424 h1:X+gTrBNOXyS1b6PjOs7iJspQiBpxqlmX91PpDL5qdgw= +github.com/wundergraph/cosmo/router v0.0.0-20250107115408-cdd3d47d6424/go.mod h1:XR62DDeHO2/vGppFRFlLGB2qLIViiUu1zg9vmYOo87M= github.com/wundergraph/cosmo/router-tests v0.0.0-20241213115435-a249dba8c52a h1:GVLe85f5g+G0IOorDBBNTfm5Ua9DO0vuVY7ReSTOEbQ= github.com/wundergraph/cosmo/router-tests v0.0.0-20241213115435-a249dba8c52a/go.mod h1:I+SFviFnd3BHlPmYn+ckmzQyDB9+/c8RZJo4t6VQAds= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.138 h1:GS0V4Yv8H3b8CLouc0ZWucgSZXgbw20lA4hkth8eoJM= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.138/go.mod h1:5DhrjJAM+S+g6Bv4iogzhM+zjvZtxT/pElk+vsXp72c= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.138.0.20250106145350-20e4f82cea6b h1:DnIV7YVjrPcrJj2awt8M1F++ql6EfC0hB0Or0m4OXx4= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.138.0.20250106145350-20e4f82cea6b/go.mod h1:B7eV0Qh8Lop9QzIOQcsvKp3S0ejfC6mgyWoJnI917yQ= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -404,8 +406,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/router-tests/go.mod b/router-tests/go.mod index e041c17c5c..f670a7508e 100644 --- a/router-tests/go.mod +++ b/router-tests/go.mod @@ -24,9 +24,9 @@ require ( github.com/tidwall/gjson v1.18.0 github.com/twmb/franz-go v1.16.1 github.com/twmb/franz-go/pkg/kadm v1.11.0 - github.com/wundergraph/cosmo/demo v0.0.0-20250106104112-39128d50b761 - github.com/wundergraph/cosmo/router v0.0.0-20250106104112-39128d50b761 - github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.138.0.20250106145350-20e4f82cea6b + github.com/wundergraph/cosmo/demo v0.0.0-20250107115408-cdd3d47d6424 + github.com/wundergraph/cosmo/router v0.0.0-20250107115408-cdd3d47d6424 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.139 go.opentelemetry.io/otel v1.28.0 go.opentelemetry.io/otel/sdk v1.28.0 go.opentelemetry.io/otel/sdk/metric v1.28.0 diff --git a/router-tests/go.sum b/router-tests/go.sum index c283a9a630..d6bd018e95 100644 --- a/router-tests/go.sum +++ b/router-tests/go.sum @@ -358,8 +358,8 @@ github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0 github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww= github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTBjW+SZK4mhxTTBVpxcqeBgWF1Rfmltbfk= github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.138.0.20250106145350-20e4f82cea6b h1:DnIV7YVjrPcrJj2awt8M1F++ql6EfC0hB0Or0m4OXx4= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.138.0.20250106145350-20e4f82cea6b/go.mod h1:B7eV0Qh8Lop9QzIOQcsvKp3S0ejfC6mgyWoJnI917yQ= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.139 h1:ZxdsKeD3igrOpJtpyUk+Y9jC+///mj2MN/t9mDeX/7E= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.139/go.mod h1:B7eV0Qh8Lop9QzIOQcsvKp3S0ejfC6mgyWoJnI917yQ= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/router-tests/ratelimit_test.go b/router-tests/ratelimit_test.go index 6ac8b70426..c83b2d6f0a 100644 --- a/router-tests/ratelimit_test.go +++ b/router-tests/ratelimit_test.go @@ -498,6 +498,177 @@ func TestRateLimit(t *testing.T) { require.Equal(t, fmt.Sprintf(`{"errors":[{"message":"Rate limit exceeded"}],"data":null,"extensions":{"rateLimit":{"key":"%s","requestRate":2,"remaining":0,"retryAfterMs":1234,"resetAfterMs":1234}}}`, key), res.Body) }) }) + t.Run("enabled - above limit - hide stats", func(t *testing.T) { + t.Parallel() + + key := uuid.New().String() + t.Cleanup(func() { + client := redis.NewClient(&redis.Options{Addr: "localhost:6379", Password: "test"}) + del := client.Del(context.Background(), key) + require.NoError(t, del.Err()) + }) + testenv.Run(t, &testenv.Config{ + RouterOptions: []core.Option{ + core.WithRateLimitConfig(&config.RateLimitConfiguration{ + Enabled: true, + Strategy: "simple", + SimpleStrategy: config.RateLimitSimpleStrategy{ + Rate: 2, + Burst: 2, + Period: time.Second * 2, + RejectExceedingRequests: false, + HideStatsFromResponseExtension: true, + }, + Storage: config.RedisConfiguration{ + Url: "redis://localhost:6379", + KeyPrefix: key, + }, + Debug: true, + }), + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query ($n:Int!) { employee(id:$n) { id details { forename surname } } }`, + Variables: json.RawMessage(`{"n":1}`), + }) + require.Equal(t, `{"data":{"employee":{"id":1,"details":{"forename":"Jens","surname":"Neuse"}}}}`, res.Body) + }) + }) + t.Run("enabled - above limit - hide stats - code enabled", func(t *testing.T) { + t.Parallel() + + key := uuid.New().String() + t.Cleanup(func() { + client := redis.NewClient(&redis.Options{Addr: "localhost:6379", Password: "test"}) + del := client.Del(context.Background(), key) + require.NoError(t, del.Err()) + }) + testenv.Run(t, &testenv.Config{ + RouterOptions: []core.Option{ + core.WithRateLimitConfig(&config.RateLimitConfiguration{ + Enabled: true, + Strategy: "simple", + SimpleStrategy: config.RateLimitSimpleStrategy{ + Rate: 1, + Burst: 1, + Period: time.Second * 2, + RejectExceedingRequests: false, + HideStatsFromResponseExtension: true, + }, + Storage: config.RedisConfiguration{ + Url: "redis://localhost:6379", + KeyPrefix: key, + }, + Debug: true, + ErrorExtensionCode: config.RateLimitErrorExtensionCode{ + Enabled: true, + Code: "RATE_LIMIT_EXCEEDED", + }, + }), + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query ($n:Int!) { employee(id:$n) { id details { forename surname } } }`, + Variables: json.RawMessage(`{"n":1}`), + }) + require.Equal(t, `{"data":{"employee":{"id":1,"details":{"forename":"Jens","surname":"Neuse"}}}}`, res.Body) + res, err := xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{ + Query: `query ($n:Int!) { employee(id:$n) { id details { forename surname } } }`, + Variables: json.RawMessage(`{"n":1}`), + }) + require.NoError(t, err) + require.Equal(t, `{"errors":[{"message":"Rate limit exceeded for Subgraph 'employees'.","extensions":{"code":"RATE_LIMIT_EXCEEDED"}}],"data":{"employee":null}}`, res.Body) + }) + }) + t.Run("enabled - above limit - hide stats - code enabled - reject", func(t *testing.T) { + t.Parallel() + + key := uuid.New().String() + t.Cleanup(func() { + client := redis.NewClient(&redis.Options{Addr: "localhost:6379", Password: "test"}) + del := client.Del(context.Background(), key) + require.NoError(t, del.Err()) + }) + testenv.Run(t, &testenv.Config{ + RouterOptions: []core.Option{ + core.WithRateLimitConfig(&config.RateLimitConfiguration{ + Enabled: true, + Strategy: "simple", + SimpleStrategy: config.RateLimitSimpleStrategy{ + Rate: 1, + Burst: 1, + Period: time.Second * 2, + RejectExceedingRequests: true, + HideStatsFromResponseExtension: true, + }, + Storage: config.RedisConfiguration{ + Url: "redis://localhost:6379", + KeyPrefix: key, + }, + Debug: true, + ErrorExtensionCode: config.RateLimitErrorExtensionCode{ + Enabled: true, + Code: "RATE_LIMIT_EXCEEDED", + }, + }), + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query ($n:Int!) { employee(id:$n) { id details { forename surname } } }`, + Variables: json.RawMessage(`{"n":1}`), + }) + require.Equal(t, `{"data":{"employee":{"id":1,"details":{"forename":"Jens","surname":"Neuse"}}}}`, res.Body) + res, err := xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{ + Query: `query ($n:Int!) { employee(id:$n) { id details { forename surname } } }`, + Variables: json.RawMessage(`{"n":1}`), + }) + require.NoError(t, err) + require.Equal(t, `{"errors":[{"message":"Rate limit exceeded","extensions":{"code":"RATE_LIMIT_EXCEEDED"}}],"data":null}`, res.Body) + }) + }) + t.Run("enabled - above limit - hide stats with reject", func(t *testing.T) { + t.Parallel() + + key := uuid.New().String() + t.Cleanup(func() { + client := redis.NewClient(&redis.Options{Addr: "localhost:6379", Password: "test"}) + del := client.Del(context.Background(), key) + require.NoError(t, del.Err()) + }) + testenv.Run(t, &testenv.Config{ + NoRetryClient: true, + RouterOptions: []core.Option{ + core.WithRateLimitConfig(&config.RateLimitConfiguration{ + Enabled: true, + Strategy: "simple", + SimpleStrategy: config.RateLimitSimpleStrategy{ + Rate: 1, + Burst: 1, + Period: time.Second * 2, + RejectExceedingRequests: true, + HideStatsFromResponseExtension: true, + }, + Storage: config.RedisConfiguration{ + Url: "redis://localhost:6379", + KeyPrefix: key, + }, + Debug: true, + }), + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query ($n:Int!) { employee(id:$n) { id details { forename surname } } }`, + Variables: json.RawMessage(`{"n":1}`), + }) + require.Equal(t, `{"data":{"employee":{"id":1,"details":{"forename":"Jens","surname":"Neuse"}}}}`, res.Body) + res, err := xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{ + Query: `query ($n:Int!) { employee(id:$n) { id details { forename surname } } }`, + Variables: json.RawMessage(`{"n":1}`), + }) + require.NoError(t, err) + require.Equal(t, `{"errors":[{"message":"Rate limit exceeded"}],"data":null}`, res.Body) + }) + }) } const ( diff --git a/router/core/errors.go b/router/core/errors.go index c05682f054..966aebaaa5 100644 --- a/router/core/errors.go +++ b/router/core/errors.go @@ -49,6 +49,7 @@ type ( Authorization json.RawMessage `json:"authorization,omitempty"` Trace json.RawMessage `json:"trace,omitempty"` StatusCode int `json:"statusCode,omitempty"` + Code string `json:"code,omitempty"` } ) diff --git a/router/core/graphql_handler.go b/router/core/graphql_handler.go index 463720472d..5d344d378c 100644 --- a/router/core/graphql_handler.go +++ b/router/core/graphql_handler.go @@ -245,12 +245,16 @@ func (h *GraphQLHandler) configureRateLimiting(ctx *resolve.Context) *resolve.Co ctx.SetRateLimiter(h.rateLimiter) ctx.RateLimitOptions = resolve.RateLimitOptions{ Enable: true, - IncludeStatsInResponseExtension: true, + IncludeStatsInResponseExtension: !h.rateLimitConfig.SimpleStrategy.HideStatsFromResponseExtension, Rate: h.rateLimitConfig.SimpleStrategy.Rate, Burst: h.rateLimitConfig.SimpleStrategy.Burst, Period: h.rateLimitConfig.SimpleStrategy.Period, RateLimitKey: h.rateLimitConfig.Storage.KeyPrefix, RejectExceedingRequests: h.rateLimitConfig.SimpleStrategy.RejectExceedingRequests, + ErrorExtensionCode: resolve.RateLimitErrorExtensionCode{ + Enabled: h.rateLimitConfig.ErrorExtensionCode.Enabled, + Code: h.rateLimitConfig.ErrorExtensionCode.Code, + }, } return WithRateLimiterStats(ctx) } @@ -284,17 +288,24 @@ func (h *GraphQLHandler) WriteError(ctx *resolve.Context, err error, res *resolv response.Errors[0].Message = errMerge.Error() case errorTypeRateLimit: response.Errors[0].Message = "Rate limit exceeded" - buf := bytes.NewBuffer(make([]byte, 0, 1024)) - err = h.rateLimiter.RenderResponseExtension(ctx, buf) - if err != nil { - requestLogger.Error("unable to render rate limit stats", zap.Error(err)) - if isHttpResponseWriter { - httpWriter.WriteHeader(http.StatusInternalServerError) + if h.rateLimitConfig.ErrorExtensionCode.Enabled { + response.Errors[0].Extensions = &Extensions{ + Code: h.rateLimitConfig.ErrorExtensionCode.Code, } - return } - response.Extensions = &Extensions{ - RateLimit: buf.Bytes(), + if !h.rateLimitConfig.SimpleStrategy.HideStatsFromResponseExtension { + buf := bytes.NewBuffer(make([]byte, 0, 1024)) + err = h.rateLimiter.RenderResponseExtension(ctx, buf) + if err != nil { + requestLogger.Error("unable to render rate limit stats", zap.Error(err)) + if isHttpResponseWriter { + httpWriter.WriteHeader(http.StatusInternalServerError) + } + return + } + response.Extensions = &Extensions{ + RateLimit: buf.Bytes(), + } } if isHttpResponseWriter { httpWriter.WriteHeader(h.rateLimiter.RejectStatusCode()) diff --git a/router/core/ratelimiter.go b/router/core/ratelimiter.go index 05826d23f6..c76fc4fb8e 100644 --- a/router/core/ratelimiter.go +++ b/router/core/ratelimiter.go @@ -37,6 +37,9 @@ func NewCosmoRateLimiter(opts *CosmoRateLimiterOptions) (rl *CosmoRateLimiter, e debug: opts.Debug, rejectStatusCode: opts.RejectStatusCode, } + if rl.rejectStatusCode == 0 { + rl.rejectStatusCode = 200 + } if opts.KeySuffixExpression != "" { rl.keySuffixProgram, err = expr.CompileStringExpression(opts.KeySuffixExpression) if err != nil { diff --git a/router/go.mod b/router/go.mod index 16c19ca2e1..36d709fbf8 100644 --- a/router/go.mod +++ b/router/go.mod @@ -34,7 +34,7 @@ require ( github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 github.com/twmb/franz-go v1.16.1 - github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.138.0.20250106145350-20e4f82cea6b + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.139 // Do not upgrade, it renames attributes we rely on go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 go.opentelemetry.io/contrib/propagators/b3 v1.23.0 diff --git a/router/go.sum b/router/go.sum index 1766fb90ab..77f37d8256 100644 --- a/router/go.sum +++ b/router/go.sum @@ -274,8 +274,8 @@ github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0 github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww= github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTBjW+SZK4mhxTTBVpxcqeBgWF1Rfmltbfk= github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.138.0.20250106145350-20e4f82cea6b h1:DnIV7YVjrPcrJj2awt8M1F++ql6EfC0hB0Or0m4OXx4= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.138.0.20250106145350-20e4f82cea6b/go.mod h1:B7eV0Qh8Lop9QzIOQcsvKp3S0ejfC6mgyWoJnI917yQ= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.139 h1:ZxdsKeD3igrOpJtpyUk+Y9jC+///mj2MN/t9mDeX/7E= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.139/go.mod h1:B7eV0Qh8Lop9QzIOQcsvKp3S0ejfC6mgyWoJnI917yQ= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= diff --git a/router/pkg/config/config.go b/router/pkg/config/config.go index c2fec60dc4..27b1573a1c 100644 --- a/router/pkg/config/config.go +++ b/router/pkg/config/config.go @@ -427,8 +427,14 @@ type RateLimitConfiguration struct { Storage RedisConfiguration `yaml:"storage"` // Debug ensures that retryAfter and resetAfter are set to stable values for testing // Debug also exposes the rate limit key in the response extension for debugging purposes - Debug bool `yaml:"debug" envDefault:"false" env:"RATE_LIMIT_DEBUG"` - KeySuffixExpression string `yaml:"key_suffix_expression,omitempty" env:"RATE_LIMIT_KEY_SUFFIX_EXPRESSION"` + Debug bool `yaml:"debug" envDefault:"false" env:"RATE_LIMIT_DEBUG"` + KeySuffixExpression string `yaml:"key_suffix_expression,omitempty" env:"RATE_LIMIT_KEY_SUFFIX_EXPRESSION"` + ErrorExtensionCode RateLimitErrorExtensionCode `yaml:"error_extension_code"` +} + +type RateLimitErrorExtensionCode struct { + Enabled bool `yaml:"enabled" envDefault:"true" env:"RATE_LIMIT_ERROR_EXTENSION_CODE_ENABLED"` + Code string `yaml:"code" envDefault:"RATE_LIMIT_EXCEEDED" env:"RATE_LIMIT_ERROR_EXTENSION_CODE"` } type RedisConfiguration struct { @@ -437,11 +443,12 @@ type RedisConfiguration struct { } type RateLimitSimpleStrategy struct { - Rate int `yaml:"rate" envDefault:"10" env:"RATE_LIMIT_SIMPLE_RATE"` - Burst int `yaml:"burst" envDefault:"10" env:"RATE_LIMIT_SIMPLE_BURST"` - Period time.Duration `yaml:"period" envDefault:"1s" env:"RATE_LIMIT_SIMPLE_PERIOD"` - RejectExceedingRequests bool `yaml:"reject_exceeding_requests" envDefault:"false" env:"RATE_LIMIT_SIMPLE_REJECT_EXCEEDING_REQUESTS"` - RejectStatusCode int `yaml:"reject_status_code" envDefault:"200" env:"RATE_LIMIT_SIMPLE_REJECT_STATUS_CODE"` + Rate int `yaml:"rate" envDefault:"10" env:"RATE_LIMIT_SIMPLE_RATE"` + Burst int `yaml:"burst" envDefault:"10" env:"RATE_LIMIT_SIMPLE_BURST"` + Period time.Duration `yaml:"period" envDefault:"1s" env:"RATE_LIMIT_SIMPLE_PERIOD"` + RejectExceedingRequests bool `yaml:"reject_exceeding_requests" envDefault:"false" env:"RATE_LIMIT_SIMPLE_REJECT_EXCEEDING_REQUESTS"` + RejectStatusCode int `yaml:"reject_status_code" envDefault:"200" env:"RATE_LIMIT_SIMPLE_REJECT_STATUS_CODE"` + HideStatsFromResponseExtension bool `yaml:"hide_stats_from_response_extension" envDefault:"false" env:"RATE_LIMIT_SIMPLE_HIDE_STATS_FROM_RESPONSE_EXTENSION"` } type CDNConfiguration struct { diff --git a/router/pkg/config/config.schema.json b/router/pkg/config/config.schema.json index 1f9fe6f845..62c60e2f9f 100644 --- a/router/pkg/config/config.schema.json +++ b/router/pkg/config/config.schema.json @@ -1504,6 +1504,11 @@ "type": "integer", "description": "The status code to return when the request is rejected. The default value is 200 (OK) as we're returning a well formed GraphQL response.", "default": 200 + }, + "hide_stats_from_response_extension": { + "type": "boolean", + "default": false, + "description": "Hide the rate limit stats from the response extension. If the value is true, the rate limit stats are not included in the response extension." } }, "required": ["rate", "burst", "period"] @@ -1532,6 +1537,23 @@ "key_suffix_expression": { "type": "string", "description": "The expression to define a key suffix for the rate limit, e.g. by using request headers, claims, or a combination of both with a fallback strategy. The expression is specified as a string and needs to evaluate to a string. Please see https://expr-lang.org/ for more information." + }, + "error_extension_code": { + "type": "object", + "description": "If enabled, a code will be added to the extensions.code field of error objects related to rate limiting. This allows clients to easily identify if an error happened due to rate limiting.", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable the error extension code for rate limiting." + }, + "code": { + "type": "string", + "description": "The error extension code for the rate limit.", + "default": "RATE_LIMIT_EXCEEDED" + } + } } } }, diff --git a/router/pkg/config/testdata/config_defaults.json b/router/pkg/config/testdata/config_defaults.json index 366fc5b718..dc81489788 100644 --- a/router/pkg/config/testdata/config_defaults.json +++ b/router/pkg/config/testdata/config_defaults.json @@ -188,14 +188,19 @@ "Burst": 10, "Period": 1000000000, "RejectExceedingRequests": false, - "RejectStatusCode": 200 + "RejectStatusCode": 200, + "HideStatsFromResponseExtension": false }, "Storage": { "Url": "redis://localhost:6379", "KeyPrefix": "cosmo_rate_limit" }, "Debug": false, - "KeySuffixExpression": "" + "KeySuffixExpression": "", + "ErrorExtensionCode": { + "Enabled": true, + "Code": "RATE_LIMIT_EXCEEDED" + } }, "LocalhostFallbackInsideDocker": true, "CDN": { diff --git a/router/pkg/config/testdata/config_full.json b/router/pkg/config/testdata/config_full.json index c0ed66b5fb..b28e33d8ca 100644 --- a/router/pkg/config/testdata/config_full.json +++ b/router/pkg/config/testdata/config_full.json @@ -363,14 +363,19 @@ "Burst": 60, "Period": 60000000000, "RejectExceedingRequests": true, - "RejectStatusCode": 200 + "RejectStatusCode": 200, + "HideStatsFromResponseExtension": false }, "Storage": { "Url": "redis://:test@localhost:6379", "KeyPrefix": "cosmo_rate_limit" }, "Debug": false, - "KeySuffixExpression": "" + "KeySuffixExpression": "", + "ErrorExtensionCode": { + "Enabled": true, + "Code": "RATE_LIMIT_EXCEEDED" + } }, "LocalhostFallbackInsideDocker": true, "CDN": {