GeezerVison is showing github.com — Add query complexity limits and refactor GoodFaithIntrospection to use validation by andimarek · Pull Request #4256 · graphql-java/graphql-java
Conversation
andimarek
commented
Feb 22, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
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
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
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.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
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 <[email protected]>
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 <[email protected]>
…base
- Add @nullable annotations for QueryComplexityLimits parameters
- Replace shouldRunNonFragmentSpreadChecks() with shouldRunDocumentLevelRules()
- Replace fragmentSpreadVisitDepth with fragmentRetraversalDepth
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
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
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
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
- 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) <[email protected]>
A GraphQL schema always has a query type.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Assignees
No one assigned
Development
Successfully merging this pull request may close these issues.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
|