Detect introspection queries dynamically during validation · graphql-java/graphql-java@02fcf27 · GitHub
Skip to content

Commit 02fcf27

Browse files
andimarekclaude
andcommitted
Detect introspection queries dynamically during validation
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>
1 parent b23c591 commit 02fcf27

4 files changed

Lines changed: 54 additions & 69 deletions

File tree

src/main/java/graphql/GraphQL.java

Lines changed: 3 additions & 19 deletions

src/main/java/graphql/introspection/GoodFaithIntrospection.java

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,6 @@
44
import graphql.GraphQLContext;
55
import graphql.GraphQLError;
66
import graphql.PublicApi;
7-
import graphql.language.Definition;
8-
import graphql.language.Document;
9-
import graphql.language.Field;
10-
import graphql.language.OperationDefinition;
11-
import graphql.language.Selection;
12-
import graphql.language.SelectionSet;
137
import graphql.language.SourceLocation;
148
import graphql.validation.QueryComplexityLimits;
159
import org.jspecify.annotations.NullMarked;
@@ -89,33 +83,6 @@ public static boolean isEnabled(GraphQLContext graphQLContext) {
8983
return !graphQLContext.getBoolean(GOOD_FAITH_INTROSPECTION_DISABLED, false);
9084
}
9185

92-
/**
93-
* Performs a shallow scan of the document to check if any operation's top-level selections
94-
* contain introspection fields ({@code __schema} or {@code __type}).
95-
*
96-
* @param document the parsed document
97-
*
98-
* @return true if the document contains top-level introspection fields
99-
*/
100-
public static boolean containsIntrospectionFields(Document document) {
101-
for (Definition<?> definition : document.getDefinitions()) {
102-
if (definition instanceof OperationDefinition) {
103-
SelectionSet selectionSet = ((OperationDefinition) definition).getSelectionSet();
104-
if (selectionSet != null) {
105-
for (Selection<?> selection : selectionSet.getSelections()) {
106-
if (selection instanceof Field) {
107-
String name = ((Field) selection).getName();
108-
if ("__schema".equals(name) || "__type".equals(name)) {
109-
return true;
110-
}
111-
}
112-
}
113-
}
114-
}
115-
}
116-
return false;
117-
}
118-
11986
/**
12087
* Returns query complexity limits that are the minimum of the existing limits and the
12188
* good faith introspection limits. This ensures introspection queries are bounded
@@ -125,10 +92,7 @@ public static boolean containsIntrospectionFields(Document document) {
12592
*
12693
* @return complexity limits with good faith bounds applied
12794
*/
128-
public static QueryComplexityLimits goodFaithLimits(@Nullable QueryComplexityLimits existing) {
129-
if (existing == null) {
130-
existing = QueryComplexityLimits.getDefaultLimits();
131-
}
95+
public static QueryComplexityLimits goodFaithLimits(QueryComplexityLimits existing) {
13296
int maxFields = Math.min(existing.getMaxFieldsCount(), GOOD_FAITH_MAX_FIELDS_COUNT);
13397
int maxDepth = Math.min(existing.getMaxDepth(), GOOD_FAITH_MAX_DEPTH_COUNT);
13498
return QueryComplexityLimits.newLimits()

src/main/java/graphql/validation/OperationValidator.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import graphql.execution.TypeFromAST;
1515
import graphql.execution.ValuesResolver;
1616
import graphql.i18n.I18nMsg;
17+
import graphql.introspection.GoodFaithIntrospection;
1718
import graphql.introspection.Introspection.DirectiveLocation;
1819
import graphql.language.Argument;
1920
import graphql.language.AstComparator;
@@ -332,14 +333,15 @@ public class OperationValidator implements DocumentVisitor {
332333
private int fieldCount = 0;
333334
private int currentFieldDepth = 0;
334335
private int maxFieldDepthSeen = 0;
335-
private final QueryComplexityLimits complexityLimits;
336+
private QueryComplexityLimits complexityLimits;
336337
// Fragment complexity calculated lazily during first spread
337338
private final Map<String, FragmentComplexityInfo> fragmentComplexityMap = new HashMap<>();
338339
// Max depth seen during current fragment traversal (for calculating fragment's internal depth)
339340
private int fragmentTraversalMaxDepth = 0;
340341

341342
// --- State: Good Faith Introspection ---
342343
private final Map<String, Integer> introspectionFieldCounts = new HashMap<>();
344+
private boolean introspectionQueryDetected = false;
343345

344346
// --- Track whether we're in a context where fragment spread rules should run ---
345347
// fragmentRetraversalDepth == 0 means we're NOT inside a manually-traversed fragment => run non-fragment-spread checks
@@ -406,6 +408,10 @@ private boolean shouldRunOperationScopedRules() {
406408

407409
private void checkFieldCountLimit() {
408410
if (fieldCount > complexityLimits.getMaxFieldsCount()) {
411+
if (introspectionQueryDetected) {
412+
throw GoodFaithIntrospectionExceeded.tooBigOperation(
413+
"Query has " + fieldCount + " fields which exceeds maximum allowed " + complexityLimits.getMaxFieldsCount());
414+
}
409415
throw new QueryComplexityLimitsExceeded(
410416
ValidationErrorType.MaxQueryFieldsExceeded,
411417
complexityLimits.getMaxFieldsCount(),
@@ -417,6 +423,10 @@ private void checkDepthLimit(int depth) {
417423
if (depth > maxFieldDepthSeen) {
418424
maxFieldDepthSeen = depth;
419425
if (maxFieldDepthSeen > complexityLimits.getMaxDepth()) {
426+
if (introspectionQueryDetected) {
427+
throw GoodFaithIntrospectionExceeded.tooBigOperation(
428+
"Query depth " + maxFieldDepthSeen + " exceeds maximum allowed depth " + complexityLimits.getMaxDepth());
429+
}
420430
throw new QueryComplexityLimitsExceeded(
421431
ValidationErrorType.MaxQueryDepthExceeded,
422432
complexityLimits.getMaxDepth(),
@@ -629,6 +639,10 @@ private void checkGoodFaithIntrospection(Field field) {
629639
if (queryType != null && parentType.getName().equals(queryType.getName())) {
630640
if ("__schema".equals(fieldName) || "__type".equals(fieldName)) {
631641
key = parentType.getName() + "." + fieldName;
642+
if (!introspectionQueryDetected) {
643+
introspectionQueryDetected = true;
644+
complexityLimits = GoodFaithIntrospection.goodFaithLimits(complexityLimits);
645+
}
632646
}
633647
}
634648
}

src/test/groovy/graphql/introspection/GoodFaithIntrospectionTest.groovy

Lines changed: 35 additions & 12 deletions

0 commit comments

Comments
 (0)