Skip to content

Commit 2a31bd7

Browse files
committed
add maximum query depth instrumentation
1 parent ebbf281 commit 2a31bd7

15 files changed

Lines changed: 303 additions & 109 deletions

‎src/main/java/graphql/ErrorType.java‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22

33

44
public enum ErrorType {
5-
65
InvalidSyntax,
76
ValidationError,
87
DataFetchingException,
9-
MutationNotSupported
10-
8+
MutationNotSupported,
9+
ExecutionAborted
1110
}

‎src/main/java/graphql/GraphQL.java‎

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package graphql;
22

3+
import graphql.execution.AbortExecutionException;
34
import graphql.execution.AsyncExecutionStrategy;
45
import graphql.execution.AsyncSerialExecutionStrategy;
56
import graphql.execution.Execution;
@@ -381,20 +382,25 @@ public CompletableFuture<ExecutionResult> executeAsync(UnaryOperator<ExecutionIn
381382
* @return a promise to an {@link ExecutionResult} which can include errors
382383
*/
383384
public CompletableFuture<ExecutionResult> executeAsync(ExecutionInput executionInput) {
384-
log.debug("Executing request. operation name: {}. query: {}. variables {} ", executionInput.getOperationName(), executionInput.getQuery(), executionInput.getVariables());
385-
386-
InstrumentationState instrumentationState = instrumentation.createState();
387-
388-
InstrumentationExecutionParameters instrumentationParameters = new InstrumentationExecutionParameters(executionInput, instrumentationState);
389-
InstrumentationContext<ExecutionResult> executionInstrumentation = instrumentation.beginExecution(instrumentationParameters);
390-
CompletableFuture<ExecutionResult> executionResult = parseValidateAndExecute(executionInput, instrumentationState);
391-
//
392-
// finish up instrumentation
393-
executionResult = executionResult.whenComplete(executionInstrumentation::onEnd);
394-
//
395-
// allow instrumentation to tweak the result
396-
executionResult = executionResult.thenCompose(result -> instrumentation.instrumentExecutionResult(result, instrumentationParameters));
397-
return executionResult;
385+
try {
386+
log.debug("Executing request. operation name: {}. query: {}. variables {} ", executionInput.getOperationName(), executionInput.getQuery(), executionInput.getVariables());
387+
388+
InstrumentationState instrumentationState = instrumentation.createState();
389+
390+
InstrumentationExecutionParameters instrumentationParameters = new InstrumentationExecutionParameters(executionInput, this.graphQLSchema, instrumentationState);
391+
InstrumentationContext<ExecutionResult> executionInstrumentation = instrumentation.beginExecution(instrumentationParameters);
392+
CompletableFuture<ExecutionResult> executionResult = parseValidateAndExecute(executionInput, instrumentationState);
393+
//
394+
// finish up instrumentation
395+
executionResult = executionResult.whenComplete(executionInstrumentation::onEnd);
396+
//
397+
// allow instrumentation to tweak the result
398+
executionResult = executionResult.thenCompose(result -> instrumentation.instrumentExecutionResult(result, instrumentationParameters));
399+
return executionResult;
400+
} catch (AbortExecutionException abortException) {
401+
ExecutionResultImpl executionResult = new ExecutionResultImpl(abortException);
402+
return CompletableFuture.completedFuture(executionResult);
403+
}
398404
}
399405

400406

@@ -425,7 +431,7 @@ private PreparsedDocumentEntry parseAndValidate(ExecutionInput executionInput, I
425431
}
426432

427433
private ParseResult parse(ExecutionInput executionInput, InstrumentationState instrumentationState) {
428-
InstrumentationContext<Document> parseInstrumentation = instrumentation.beginParse(new InstrumentationExecutionParameters(executionInput, instrumentationState));
434+
InstrumentationContext<Document> parseInstrumentation = instrumentation.beginParse(new InstrumentationExecutionParameters(executionInput, this.graphQLSchema, instrumentationState));
429435

430436
Parser parser = new Parser();
431437
Document document;
@@ -441,7 +447,7 @@ private ParseResult parse(ExecutionInput executionInput, InstrumentationState in
441447
}
442448

443449
private List<ValidationError> validate(ExecutionInput executionInput, Document document, InstrumentationState instrumentationState) {
444-
InstrumentationContext<List<ValidationError>> validationCtx = instrumentation.beginValidation(new InstrumentationValidationParameters(executionInput, document, instrumentationState));
450+
InstrumentationContext<List<ValidationError>> validationCtx = instrumentation.beginValidation(new InstrumentationValidationParameters(executionInput, document, this.graphQLSchema, instrumentationState));
445451

446452
Validator validator = new Validator();
447453
List<ValidationError> validationErrors = validator.validateDocument(graphQLSchema, document);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package graphql.analysis;
2+
3+
import graphql.PublicApi;
4+
import graphql.execution.AbortExecutionException;
5+
import graphql.execution.instrumentation.InstrumentationContext;
6+
import graphql.execution.instrumentation.NoOpInstrumentation;
7+
import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters;
8+
import graphql.language.NodeUtil;
9+
import graphql.validation.ValidationError;
10+
11+
import java.util.List;
12+
13+
/**
14+
* Prevents execution if the query depth is greater than the specified maxDepth
15+
*/
16+
@PublicApi
17+
public class MaximumQueryDepthInstrumentation extends NoOpInstrumentation {
18+
19+
private int maxDepth;
20+
21+
public MaximumQueryDepthInstrumentation(int maxDepth) {
22+
this.maxDepth = maxDepth;
23+
}
24+
25+
@Override
26+
public InstrumentationContext<List<ValidationError>> beginValidation(InstrumentationValidationParameters parameters) {
27+
return (result, throwable) -> {
28+
NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(parameters.getDocument(), parameters.getOperation());
29+
QueryTraversal queryTraversal = new QueryTraversal(
30+
getOperationResult.operationDefinition,
31+
parameters.getSchema(),
32+
getOperationResult.fragmentsByName,
33+
parameters.getVariables()
34+
);
35+
int depth = (int) queryTraversal.reduce((env, acc) -> Math.max(getPathLength(env.getPath()), (Integer) acc), 0);
36+
if (depth > maxDepth) {
37+
throw new AbortExecutionException("maximum query depth exceeded " + depth + " > " + maxDepth);
38+
}
39+
};
40+
}
41+
42+
private int getPathLength(VisitPath path) {
43+
int length = 1;
44+
while (path != null) {
45+
path = path.getParentPath();
46+
length++;
47+
}
48+
return length;
49+
}
50+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package graphql.analysis;
2+
3+
import graphql.Internal;
4+
5+
@Internal
6+
@FunctionalInterface
7+
public interface QueryReducer<T> {
8+
9+
T reduceField(QueryVisitorEnvironment reducerEnvironment, T acc);
10+
}

src/main/java/graphql/visitor/QueryTraversal.java renamed to src/main/java/graphql/analysis/QueryTraversal.java

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
package graphql.visitor;
1+
package graphql.analysis;
22

3+
import graphql.Internal;
34
import graphql.execution.ConditionalNodes;
4-
import graphql.language.Document;
55
import graphql.language.Field;
66
import graphql.language.FragmentDefinition;
77
import graphql.language.FragmentSpread;
@@ -18,54 +18,56 @@
1818
import java.util.LinkedHashMap;
1919
import java.util.Map;
2020

21+
@Internal
2122
public class QueryTraversal {
2223

2324

24-
private Document document;
2525
private OperationDefinition operationDefinition;
2626
private GraphQLSchema schema;
2727
private Map<String, FragmentDefinition> fragmentsByName = new LinkedHashMap<>();
2828
private Map<String, Object> variables;
2929

3030
private ConditionalNodes conditionalNodes = new ConditionalNodes();
3131

32-
private QueryVisitor visitor;
3332

34-
public QueryTraversal(Document document,
35-
OperationDefinition operationDefinition,
33+
public QueryTraversal(OperationDefinition operationDefinition,
3634
GraphQLSchema schema,
3735
Map<String, FragmentDefinition> fragmentsByName,
38-
Map<String, Object> variables,
39-
QueryVisitor visitor) {
40-
this.document = document;
36+
Map<String, Object> variables) {
4137
this.operationDefinition = operationDefinition;
4238
this.schema = schema;
4339
this.fragmentsByName = fragmentsByName;
4440
this.variables = variables;
45-
this.visitor = visitor;
4641
}
4742

48-
public void traverse() {
49-
visit(operationDefinition.getSelectionSet(), schema.getQueryType(), null);
43+
public void visit(QueryVisitor visitor) {
44+
visitImpl(visitor, operationDefinition.getSelectionSet(), schema.getQueryType(), null);
45+
}
46+
47+
public Object reduce(QueryReducer queryReducer, Object initialValue) {
48+
// compiler hack to make acc final and mutable :-)
49+
final Object[] acc = {initialValue};
50+
visit((env) -> acc[0] = queryReducer.reduceField(env, acc[0]));
51+
return acc[0];
5052
}
5153

5254

53-
private void visit(SelectionSet selectionSet, GraphQLCompositeType type, VisitPath path) {
55+
private void visitImpl(QueryVisitor visitor, SelectionSet selectionSet, GraphQLCompositeType type, VisitPath path) {
5456

5557
for (Selection selection : selectionSet.getSelections()) {
5658
if (selection instanceof Field) {
5759
GraphQLFieldsContainer fieldsContainer = (GraphQLFieldsContainer) type;
5860
GraphQLFieldDefinition fieldDefinition = fieldsContainer.getFieldDefinition(((Field) selection).getName());
59-
visitField((Field) selection, fieldDefinition, type, path);
61+
visitField(visitor, (Field) selection, fieldDefinition, type, path);
6062
} else if (selection instanceof InlineFragment) {
61-
visitInlineFragment((InlineFragment) selection, type, path);
63+
visitInlineFragment(visitor, (InlineFragment) selection, type, path);
6264
} else if (selection instanceof FragmentSpread) {
63-
visitFragmentSpread((FragmentSpread) selection, path);
65+
visitFragmentSpread(visitor, (FragmentSpread) selection, path);
6466
}
6567
}
6668
}
6769

68-
private void visitFragmentSpread(FragmentSpread fragmentSpread, VisitPath path) {
70+
private void visitFragmentSpread(QueryVisitor visitor, FragmentSpread fragmentSpread, VisitPath path) {
6971
if (!conditionalNodes.shouldInclude(this.variables, fragmentSpread.getDirectives())) {
7072
return;
7173
}
@@ -75,11 +77,11 @@ private void visitFragmentSpread(FragmentSpread fragmentSpread, VisitPath path)
7577
return;
7678
}
7779
GraphQLCompositeType typeCondition = (GraphQLCompositeType) schema.getType(fragmentDefinition.getTypeCondition().getName());
78-
visit(fragmentDefinition.getSelectionSet(), typeCondition, path);
80+
visitImpl(visitor, fragmentDefinition.getSelectionSet(), typeCondition, path);
7981
}
8082

8183

82-
private void visitInlineFragment(InlineFragment inlineFragment, GraphQLCompositeType parentType, VisitPath path) {
84+
private void visitInlineFragment(QueryVisitor visitor, InlineFragment inlineFragment, GraphQLCompositeType parentType, VisitPath path) {
8385
if (!conditionalNodes.shouldInclude(variables, inlineFragment.getDirectives())) {
8486
return;
8587
}
@@ -92,17 +94,17 @@ private void visitInlineFragment(InlineFragment inlineFragment, GraphQLComposite
9294
fragmentCondition = parentType;
9395
}
9496
// for unions we only have other fragments inside
95-
visit(inlineFragment.getSelectionSet(), fragmentCondition, path);
97+
visitImpl(visitor, inlineFragment.getSelectionSet(), fragmentCondition, path);
9698
}
9799

98-
private void visitField(Field field, GraphQLFieldDefinition fieldDefinition, GraphQLCompositeType parentType, VisitPath parentPath) {
100+
private void visitField(QueryVisitor visitor, Field field, GraphQLFieldDefinition fieldDefinition, GraphQLCompositeType parentType, VisitPath parentPath) {
99101
if (!conditionalNodes.shouldInclude(variables, field.getDirectives())) {
100102
return;
101103
}
102-
visitor.visitField(field, fieldDefinition, parentType, parentPath);
104+
visitor.visitField(new QueryVisitorEnvironment(field, fieldDefinition, parentType, parentPath));
103105
if (fieldDefinition.getType() instanceof GraphQLCompositeType) {
104106
VisitPath newPath = new VisitPath(field, fieldDefinition, parentType, parentPath);
105-
visit(field.getSelectionSet(), (GraphQLCompositeType) fieldDefinition.getType(), newPath);
107+
visitImpl(visitor, field.getSelectionSet(), (GraphQLCompositeType) fieldDefinition.getType(), newPath);
106108
}
107109
}
108110

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package graphql.analysis;
2+
3+
import graphql.Internal;
4+
5+
@Internal
6+
public interface QueryVisitor {
7+
8+
void visitField(QueryVisitorEnvironment queryVisitorEnvironment);
9+
10+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package graphql.analysis;
2+
3+
import graphql.Internal;
4+
import graphql.language.Field;
5+
import graphql.schema.GraphQLCompositeType;
6+
import graphql.schema.GraphQLFieldDefinition;
7+
8+
@Internal
9+
public class QueryVisitorEnvironment {
10+
private final Field field;
11+
private final GraphQLFieldDefinition fieldDefinition;
12+
private final GraphQLCompositeType parent;
13+
private final VisitPath path;
14+
15+
public QueryVisitorEnvironment(Field field, GraphQLFieldDefinition fieldDefinition, GraphQLCompositeType parent, VisitPath path) {
16+
this.field = field;
17+
this.fieldDefinition = fieldDefinition;
18+
this.parent = parent;
19+
this.path = path;
20+
}
21+
22+
public Field getField() {
23+
return field;
24+
}
25+
26+
public GraphQLFieldDefinition getFieldDefinition() {
27+
return fieldDefinition;
28+
}
29+
30+
public GraphQLCompositeType getParent() {
31+
return parent;
32+
}
33+
34+
public VisitPath getPath() {
35+
return path;
36+
}
37+
38+
@Override
39+
public String toString() {
40+
return "QueryVisitorEnvironment{" +
41+
"field=" + field +
42+
", fieldDefinition=" + fieldDefinition +
43+
", parent=" + parent +
44+
", path=" + path +
45+
'}';
46+
}
47+
}

src/main/java/graphql/visitor/VisitPath.java renamed to src/main/java/graphql/analysis/VisitPath.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
package graphql.visitor;
1+
package graphql.analysis;
22

3+
import graphql.Internal;
34
import graphql.language.Field;
45
import graphql.schema.GraphQLCompositeType;
56
import graphql.schema.GraphQLFieldDefinition;
67

8+
@Internal
79
public class VisitPath {
810
private final Field field;
911
private final GraphQLFieldDefinition fieldDefinition;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package graphql.execution;
2+
3+
import graphql.ErrorType;
4+
import graphql.GraphQLError;
5+
import graphql.GraphQLException;
6+
import graphql.PublicApi;
7+
import graphql.language.SourceLocation;
8+
9+
import java.util.List;
10+
11+
@PublicApi
12+
public class AbortExecutionException extends GraphQLException implements GraphQLError {
13+
14+
public AbortExecutionException() {
15+
}
16+
17+
public AbortExecutionException(String message) {
18+
super(message);
19+
}
20+
21+
public AbortExecutionException(String message, Throwable cause) {
22+
super(message, cause);
23+
}
24+
25+
public AbortExecutionException(Throwable cause) {
26+
super(cause);
27+
}
28+
29+
@Override
30+
public List<SourceLocation> getLocations() {
31+
return null;
32+
}
33+
34+
@Override
35+
public ErrorType getErrorType() {
36+
return ErrorType.ExecutionAborted;
37+
}
38+
}

0 commit comments

Comments
 (0)