Skip to content

Add query complexity limits and refactor GoodFaithIntrospection to use validation#4256

Merged
andimarek merged 12 commits intomasterfrom
claude/build-and-run-tests-SByLK
Mar 23, 2026
Merged

Add query complexity limits and refactor GoodFaithIntrospection to use validation#4256
andimarek merged 12 commits intomasterfrom
claude/build-and-run-tests-SByLK

Conversation

@andimarek
Copy link
Copy Markdown
Member

@andimarek andimarek commented Feb 22, 2026

Summary

Adds lightweight query complexity checking during validation and refactors GoodFaithIntrospection to use validation rules instead of ExecutableNormalizedOperation (ENO).

Query Complexity Limits

  • New QueryComplexityLimits class with maxDepth and maxFieldsCount settings
  • Default limits: depth 100, fields 100,000 (use QueryComplexityLimits.NONE to disable)
  • Configuration via GraphQLContext using QueryComplexityLimits.KEY
  • Fragment fields counted at each spread site; complexity calculated lazily during first spread traversal
  • No additional AST traversal — complexity tracked during normal validation
  • New validation error types: MaxQueryDepthExceeded, MaxQueryFieldsExceeded

GoodFaithIntrospection Refactor

  • Replaced ENO-based introspection checking with a validation rule (GOOD_FAITH_INTROSPECTION)
  • Introspection queries are detected dynamically during validation when __schema or __type is encountered on the Query type — no pre-scan of the document needed
  • On detection, complexity limits are tightened to good faith bounds (500 fields, depth 20) and subsequent limit breaches throw GoodFaithIntrospectionExceeded directly
  • Correctly detects introspection fields inside inline fragments and fragment spreads (the old ENO-based approach handled this, but any AST pre-scan would miss them)
  • Same field instance limits as before: Query.__schema, Query.__type, __Type.fields/inputFields/interfaces/possibleTypes each allowed at most once

Usage

QueryComplexityLimits limits = QueryComplexityLimits.newLimits()
    .maxDepth(10)
    .maxFieldsCount(100)
    .build();

ExecutionInput input = ExecutionInput.newExecutionInput()
    .query(query)
    .graphQLContext(ctx -> ctx.put(QueryComplexityLimits.KEY, limits))
    .build();

Breaking changes

Behavioral changes

  • Default query complexity limits are now enforced. All queries are subject to default limits of depth 100 and 100,000 fields. Queries exceeding these limits will receive MaxQueryDepthExceeded or MaxQueryFieldsExceeded validation errors. Use QueryComplexityLimits.NONE via GraphQLContext to disable, or call QueryComplexityLimits.setDefaultLimits(QueryComplexityLimits.NONE) globally.
  • GoodFaithIntrospection now runs during validation instead of execution. Bad faith introspection errors are now returned as validation errors rather than being detected at execution time via ENO. The same checks are enforced but they happen earlier in the pipeline.

GoodFaithIntrospection (@PublicApi)

  • Removed checkIntrospection(ExecutionContext) — was the ENO-based entry point, no longer needed
  • Removed ALLOWED_FIELD_INSTANCES map — field instance limits are now enforced inside the validator
  • Added isEnabled(GraphQLContext) — checks whether good faith introspection is enabled for a request
  • Added goodFaithLimits(QueryComplexityLimits) — computes the effective limits for introspection queries
  • BadFaithIntrospectionError.getLocations() return type changed to @Nullable List<SourceLocation>

ValidationErrorType (@PublicApi)

  • Added MaxQueryDepthExceeded enum value
  • Added MaxQueryFieldsExceeded enum value

OperationValidationRule (@PublicApi)

  • Added GOOD_FAITH_INTROSPECTION enum value

ParseAndValidate (@PublicApi)

  • Added overload validate(GraphQLSchema, Document, Predicate, Locale, QueryComplexityLimits) accepting optional complexity limits

New public classes

  • QueryComplexityLimits (@PublicApi) — configuration for depth and field count limits
  • QueryComplexityLimits.Builder (@PublicApi) — builder for QueryComplexityLimits

Test plan

  • Existing GoodFaithIntrospection tests pass (bad faith detection, disable/enable, deep queries)
  • New tests for introspection via inline fragments and fragment spreads
  • New tests for tooBigOperation path (field count and depth limit exceeded)
  • New tests for custom user limits combined with good faith limits
  • QueryComplexityLimits unit tests for depth and field count enforcement
  • Benchmark tests updated to use QueryComplexityLimits.NONE
  • All Java versions (11, 17, 21, 25) pass

🤖 Generated with Claude Code

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 22, 2026

Test Results

0 files   -   335  0 suites   - 335   0s ⏱️ - 5m 9s
0 tests  - 5 378  0 ✅  - 5 370  0 💤  - 8  0 ❌ ±0 
0 runs   - 5 467  0 ✅  - 5 459  0 💤  - 8  0 ❌ ±0 

Results for commit 884234a. ± Comparison against base commit 1e867c2.

♻️ This comment has been updated with latest results.

andimarek and others added 3 commits March 23, 2026 07:42
This provides a lightweight alternative to ExecutableNormalizedOperation
(ENO) for tracking query complexity during validation.

New features:
- QueryComplexityLimits class with maxDepth and maxFieldsCount settings
- Configuration via GraphQLContext using QueryComplexityLimits.KEY
- Fragment fields counted at each spread site (like ENO)
- Depth tracking measures nested Field nodes
- New validation error types: MaxQueryDepthExceeded, MaxQueryFieldsExceeded

Implementation notes:
- Fragment complexity is calculated lazily during first spread traversal
- No additional AST traversal needed - complexity tracked during normal
  validation traversal
- Subsequent spreads of the same fragment add the stored complexity

Usage:
```java
QueryComplexityLimits limits = QueryComplexityLimits.newLimits()
    .maxDepth(10)
    .maxFieldsCount(100)
    .build();

ExecutionInput input = ExecutionInput.newExecutionInput()
    .query(query)
    .graphQLContext(ctx -> ctx.put(QueryComplexityLimits.KEY, limits))
    .build();
```

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move introspection abuse detection from execution-time ENO creation to
the validation layer. This eliminates the expensive
ExecutableNormalizedOperation construction for every introspection query.

The validator now enforces two checks when GOOD_FAITH_INTROSPECTION is
enabled: field repetition (__schema/__type max once, __Type cycle fields
max once) and tightened complexity limits (500 fields, 20 depth).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…base

- Add @nullable annotations for QueryComplexityLimits parameters
- Replace shouldRunNonFragmentSpreadChecks() with shouldRunDocumentLevelRules()
- Replace fragmentSpreadVisitDepth with fragmentRetraversalDepth

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@andimarek andimarek force-pushed the claude/build-and-run-tests-SByLK branch from 884234a to 97d10d2 Compare March 22, 2026 21:44
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 22, 2026

Test Report

Test Results

Java Version Total Passed Failed Errors Skipped
Java 11 5732 (+24 🟢) 5676 (+24 🟢) 0 (±0) 0 (±0) 56 (±0)
Java 17 5732 (+24 🟢) 5675 (+24 🟢) 0 (±0) 0 (±0) 57 (±0)
Java 21 5732 (+24 🟢) 5675 (+24 🟢) 0 (±0) 0 (±0) 57 (±0)
Java 25 5732 (+24 🟢) 5675 (+24 🟢) 0 (±0) 0 (±0) 57 (±0)
jcstress 32 (±0) 32 (±0) 0 (±0) 0 (±0) 0 (±0)
Total 22960 (+96 🟢) 22733 (+96 🟢) 0 (±0) 0 (±0) 227 (±0)

Code Coverage (Java 25)

Metric Covered Missed Coverage vs Master
Lines 28899 3119 90.3% ±0.0%
Branches 8419 1505 84.8% +0.1% 🟢
Methods 7730 1222 86.3% ±0.0%

Changed Class Coverage (11 classes)

Class Line Branch Method
g.GraphQL +0.1% 🟢 +2.1% 🟢 +0.1% 🟢
g.i.GoodFaithIntrospection
$BadFaithIntrospectionError
+11.1% 🟢 ±0.0% +14.3% 🟢
g.ParseAndValidate +0.5% 🟢 ±0.0% +1.8% 🟢
g.v.FragmentComplexityInfo +85.7% 🟢 ±0.0% +75.0% 🟢
g.v.GoodFaithIntrospectionExceeded +100.0% 🟢 +100.0% 🟢 +100.0% 🟢
g.v.OperationValidator +0.2% 🟢 +0.6% 🟢 ±0.0%
g.v.QueryComplexityLimits +92.9% 🟢 ±0.0% +87.5% 🟢
g.v.QueryComplexityLimits
$Builder
+100.0% 🟢 +100.0% 🟢 +100.0% 🟢
g.v.QueryComplexityLimitsExceeded +100.0% 🟢 ±0.0% +100.0% 🟢
g.v.ValidationContext +0.3% 🟢 ±0.0% +0.6% 🟢
g.v.Validator +2.1% 🟢 ±0.0% +1.8% 🟢

Full HTML report: build artifact jacoco-html-report

Updated: 2026-03-23 01:30:33 UTC

andimarek and others added 2 commits March 23, 2026 08:00
The OverlappingFieldsCanBeMergedBenchmarkTest uses intentionally large
queries (108k fields, depth 101) that exceed default complexity limits.
Pass QueryComplexityLimits.NONE to bypass the limits in these tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The refactored GoodFaithIntrospection validation had uncovered code paths
for queries that exceed complexity limits (field count or depth) without
triggering the tooManyFields cycle check. These tests exercise:
- Wide introspection query exceeding field count limit (>500 fields)
- Deep introspection query exceeding depth limit (>20 levels via ofType)
- Custom user limits combined with good faith limits

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@andimarek andimarek force-pushed the claude/build-and-run-tests-SByLK branch from cdd531a to deb35be Compare March 22, 2026 22:51
Instead of pre-scanning the document with containsIntrospectionFields,
let checkGoodFaithIntrospection detect introspection queries at
validation time when it first encounters __schema or __type on the
Query type. At that point it tightens the complexity limits and sets
a flag so that subsequent limit breaches throw
GoodFaithIntrospectionExceeded directly.

This eliminates the pre-scan (which could miss introspection fields
hidden inside inline fragments or fragment spreads) and simplifies
GraphQL.validate().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@andimarek andimarek force-pushed the claude/build-and-run-tests-SByLK branch from b4c02fb to 901dafc Compare March 23, 2026 00:09
@andimarek andimarek changed the title Add query depth and field count limits to Validator Add query complexity limits and refactor GoodFaithIntrospection to use validation Mar 23, 2026
@andimarek andimarek added the breaking change requires a new major version to be relased label Mar 23, 2026
andimarek and others added 5 commits March 23, 2026 10:55
- Remove redundant @nonnull annotations from new validate() overload
  (class is @NullMarked, only @nullable needed for limits parameter)
- Remove redundant null check in QueryComplexityLimits.setDefaultLimits()
  (class is @NullMarked, parameter cannot be null)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
A GraphQL schema always has a query type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace "__schema", "__type", and "__Type" string literals in
checkGoodFaithIntrospection with Introspection.SchemaMetaFieldDef,
Introspection.TypeMetaFieldDef, and Introspection.__Type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bad merge in 0c7b9c3 took the one-liner branchName definition from
master (which does replaceAll inline) but kept the two-liner's
sanitizedBranchName reference from the copilot branch. The variable
branchName already includes sanitization via replaceAll('[/\\]', '-').

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers the lambda branch in GraphQL.validate() where good faith is
disabled and an existing custom rule predicate also excludes a rule,
exercising the && short-circuit path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@andimarek andimarek enabled auto-merge (rebase) March 23, 2026 01:22
@andimarek andimarek merged commit 0e77994 into master Mar 23, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change requires a new major version to be relased

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant