diff --git a/router-tests/complexity_limits_test.go b/router-tests/complexity_limits_test.go index 85e47d8f23..4e991681d3 100644 --- a/router-tests/complexity_limits_test.go +++ b/router-tests/complexity_limits_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/wundergraph/cosmo/router-tests/testenv" + "github.com/wundergraph/cosmo/router/core" "github.com/wundergraph/cosmo/router/pkg/config" "github.com/wundergraph/cosmo/router/pkg/otel" "github.com/wundergraph/cosmo/router/pkg/trace/tracetest" @@ -55,6 +56,144 @@ func TestComplexityLimits(t *testing.T) { }) }) + t.Run("limits are checked for introspection queries by default", func(t *testing.T) { + t.Parallel() + testenv.Run(t, &testenv.Config{ + RouterOptions: []core.Option{ + core.WithIntrospection(true, config.IntrospectionConfiguration{ + Enabled: true, + }), + }, + ModifySecurityConfiguration: func(c *config.SecurityConfiguration) { + if c.ComplexityLimits == nil { + c.ComplexityLimits = &config.ComplexityLimits{ + Depth: &config.ComplexityLimit{ + Enabled: true, + Limit: 1, + }, + } + } + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res, _ := xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{ + Query: ` + query IntrospectionQuery { + __schema { + types { ...FullType } + } + } + fragment FullType on __Type { + kind + name + description + fields(includeDeprecated: true) { + name + description + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } + } + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + }`, + }) + require.Equal(t, 400, res.Response.StatusCode) + require.Equal(t, `{"errors":[{"message":"The query depth 9 exceeds the max query depth allowed (1)"}]}`, res.Body) + }) + }) + + t.Run("skipped limits for introspection queries", func(t *testing.T) { + t.Parallel() + testenv.Run(t, &testenv.Config{ + RouterOptions: []core.Option{ + core.WithIntrospection(true, config.IntrospectionConfiguration{ + Enabled: true, + }), + }, + ModifySecurityConfiguration: func(c *config.SecurityConfiguration) { + if c.ComplexityLimits == nil { + c.ComplexityLimits = &config.ComplexityLimits{ + SkipIntrospection: true, + Depth: &config.ComplexityLimit{ + Enabled: true, + Limit: 1, + }, + } + } + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: ` + query IntrospectionQuery { + __schema { + types { ...FullType } + } + } + fragment FullType on __Type { + kind + name + description + fields(includeDeprecated: true) { + name + description + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } + } + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + }`, + }) + require.Contains(t, res.Body, `"types":[{"kind":"OBJECT","name":"Query","description":"","fields":[{"name":"employee","description":"","type":{"kind":"OBJECT","name":"Employee","ofType":null}`) + }) + }) + t.Run("max query depth blocks queries over the limit", func(t *testing.T) { t.Parallel() testenv.Run(t, &testenv.Config{ diff --git a/router-tests/go.mod b/router-tests/go.mod index ae72b82fb3..a2dae8f4e8 100644 --- a/router-tests/go.mod +++ b/router-tests/go.mod @@ -27,7 +27,7 @@ require ( github.com/wundergraph/cosmo/demo/pkg/subgraphs/projects v0.0.0-20250715110703-10f2e5f9c79e github.com/wundergraph/cosmo/router v0.0.0-20251030234733-8ed574a0296f github.com/wundergraph/cosmo/router-plugin v0.0.0-20250808194725-de123ba1c65e - github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.237 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.237.0.20251111144341-5d93cce1da45 go.opentelemetry.io/otel v1.36.0 go.opentelemetry.io/otel/sdk v1.36.0 go.opentelemetry.io/otel/sdk/metric v1.36.0 diff --git a/router-tests/go.sum b/router-tests/go.sum index f13a9d5244..085149c290 100644 --- a/router-tests/go.sum +++ b/router-tests/go.sum @@ -354,8 +354,8 @@ github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTB github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= github.com/wundergraph/consul/sdk v0.0.0-20250204115147-ed842a8fd301 h1:EzfKHQoTjFDDcgaECCCR2aTePqMu9QBmPbyhqIYOhV0= github.com/wundergraph/consul/sdk v0.0.0-20250204115147-ed842a8fd301/go.mod h1:wxI0Nak5dI5RvJuzGyiEK4nZj0O9X+Aw6U0tC1wPKq0= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.237 h1:5cNEANPVbWUFDEX9dqETH1fa7MIoJDFipuJLFCReSOM= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.237/go.mod h1:ErOQH1ki2+SZB8JjpTyGVnoBpg5picIyjvuWQJP4abg= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.237.0.20251111144341-5d93cce1da45 h1:yFGnY8vQ/uNzICmxmYGHrjDwEdqHjOgkF6chm45Htr0= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.237.0.20251111144341-5d93cce1da45/go.mod h1:ErOQH1ki2+SZB8JjpTyGVnoBpg5picIyjvuWQJP4abg= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= diff --git a/router/core/operation_processor.go b/router/core/operation_processor.go index eb020a785f..12260e98a1 100644 --- a/router/core/operation_processor.go +++ b/router/core/operation_processor.go @@ -1158,21 +1158,23 @@ func (o *OperationKit) Validate(skipLoader bool, remapVariables map[string]strin // ValidateQueryComplexity validates that the query complexity is within the limits set in the configuration func (o *OperationKit) ValidateQueryComplexity() (ok bool, cacheEntry ComplexityCacheEntry, err error) { - if o.operationProcessor.complexityLimits == nil { + limits := o.operationProcessor.complexityLimits + if limits == nil { return true, ComplexityCacheEntry{}, nil } if o.cache != nil && o.cache.complexityCache != nil { if cachedComplexity, found := o.cache.complexityCache.Get(o.parsedOperation.InternalID); found { - return true, cachedComplexity, o.runComplexityComparisons(o.operationProcessor.complexityLimits, cachedComplexity, o.parsedOperation.IsPersistedOperation) + return true, cachedComplexity, o.runComplexityComparisons(limits, cachedComplexity, o.parsedOperation.IsPersistedOperation) } } report := operationreport.Report{} - globalComplexityResult, rootFieldStats := operation_complexity.CalculateOperationComplexity(o.kit.doc, o.operationProcessor.executor.ClientSchema, &report) + estimator := operation_complexity.NewOperationComplexityEstimator(limits.SkipIntrospection) + globalComplexity, rootFieldStats := estimator.Do(o.kit.doc, o.operationProcessor.executor.ClientSchema, &report) cacheResult := ComplexityCacheEntry{ - Depth: globalComplexityResult.Depth, - TotalFields: globalComplexityResult.NodeCount, + Depth: globalComplexity.Depth, + TotalFields: globalComplexity.NodeCount, } for _, entry := range rootFieldStats { if entry.Alias == "" { @@ -1186,7 +1188,7 @@ func (o *OperationKit) ValidateQueryComplexity() (ok bool, cacheEntry Complexity o.cache.complexityCache.Set(o.parsedOperation.InternalID, cacheResult, 1) } - return false, cacheResult, o.runComplexityComparisons(o.operationProcessor.complexityLimits, cacheResult, o.parsedOperation.IsPersistedOperation) + return false, cacheResult, o.runComplexityComparisons(limits, cacheResult, o.parsedOperation.IsPersistedOperation) } func (o *OperationKit) runComplexityComparisons(complexityLimitConfig *config.ComplexityLimits, cachedComplexity ComplexityCacheEntry, isPersisted bool) error { diff --git a/router/core/router.go b/router/core/router.go index fd07b166cc..87db3c6039 100644 --- a/router/core/router.go +++ b/router/core/router.go @@ -521,7 +521,7 @@ func NewRouter(opts ...Option) (*Router, error) { IgnorePersistedOperations: r.securityConfiguration.DepthLimit.IgnorePersistedOperations, } } else { - r.logger.Warn("Ignoring deprecated security configuration field 'depth_limit', in favor of the `security_complexity_limits.depth` configuration") + r.logger.Warn("Ignoring deprecated security configuration field 'depth_limit', in favor of the `security.complexity_limits.depth` configuration") } } diff --git a/router/go.mod b/router/go.mod index b9b6fd518c..cf034b07a5 100644 --- a/router/go.mod +++ b/router/go.mod @@ -31,7 +31,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.237 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.237.0.20251111144341-5d93cce1da45 // Do not upgrade, it renames attributes we rely on go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 go.opentelemetry.io/contrib/propagators/b3 v1.23.0 diff --git a/router/go.sum b/router/go.sum index b50419d7bf..b349e3ac48 100644 --- a/router/go.sum +++ b/router/go.sum @@ -322,8 +322,8 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= 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.237 h1:5cNEANPVbWUFDEX9dqETH1fa7MIoJDFipuJLFCReSOM= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.237/go.mod h1:ErOQH1ki2+SZB8JjpTyGVnoBpg5picIyjvuWQJP4abg= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.237.0.20251111144341-5d93cce1da45 h1:yFGnY8vQ/uNzICmxmYGHrjDwEdqHjOgkF6chm45Htr0= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.237.0.20251111144341-5d93cce1da45/go.mod h1:ErOQH1ki2+SZB8JjpTyGVnoBpg5picIyjvuWQJP4abg= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= diff --git a/router/pkg/config/config.go b/router/pkg/config/config.go index caf7738266..223815da61 100644 --- a/router/pkg/config/config.go +++ b/router/pkg/config/config.go @@ -437,10 +437,11 @@ type ComplexityCalculationCache struct { } type ComplexityLimits struct { - Depth *ComplexityLimit `yaml:"depth"` - TotalFields *ComplexityLimit `yaml:"total_fields"` - RootFields *ComplexityLimit `yaml:"root_fields"` - RootFieldAliases *ComplexityLimit `yaml:"root_field_aliases"` + Depth *ComplexityLimit `yaml:"depth"` + TotalFields *ComplexityLimit `yaml:"total_fields"` + RootFields *ComplexityLimit `yaml:"root_fields"` + RootFieldAliases *ComplexityLimit `yaml:"root_field_aliases"` + SkipIntrospection bool `yaml:"skip_introspection" envDefault:"false"` } type ComplexityLimit struct { @@ -450,7 +451,7 @@ type ComplexityLimit struct { } func (c *ComplexityLimit) ApplyLimit(isPersistent bool) bool { - return c.Enabled && (!isPersistent || isPersistent && !c.IgnorePersistedOperations) + return c.Enabled && (!isPersistent || !c.IgnorePersistedOperations) } type OverrideRoutingURLConfiguration struct { diff --git a/router/pkg/config/config.schema.json b/router/pkg/config/config.schema.json index 7874902f96..5ad4ef0d3c 100644 --- a/router/pkg/config/config.schema.json +++ b/router/pkg/config/config.schema.json @@ -2596,6 +2596,11 @@ "default": false } } + }, + "skip_introspection": { + "type": "boolean", + "default": false, + "description": "Set to to true to skip introspection queries when calculating complexity limits." } } }, diff --git a/router/pkg/config/testdata/config_full.json b/router/pkg/config/testdata/config_full.json index f74e741d01..2e29be3175 100644 --- a/router/pkg/config/testdata/config_full.json +++ b/router/pkg/config/testdata/config_full.json @@ -709,7 +709,8 @@ "Enabled": true, "Limit": 4, "IgnorePersistedOperations": true - } + }, + "SkipIntrospection": false }, "DepthLimit": null, "ParserLimits": {