Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/java/graphql/execution/Execution.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public CompletableFuture<ExecutionResult> execute(Document document, GraphQLSche
.executionInput(executionInput)
.build();

executionContext.getGraphQLContext().put(ResultNodesInfo.RESULT_NODES_INFO, executionContext.getResultNodesInfo());

InstrumentationExecutionParameters parameters = new InstrumentationExecutionParameters(
executionInput, graphQLSchema, instrumentationState
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/graphql/execution/ExecutionContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public class ExecutionContext {
private final ValueUnboxer valueUnboxer;
private final ExecutionInput executionInput;
private final Supplier<ExecutableNormalizedOperation> queryTree;
private final ResultNodesInfo resultNodesInfo = new ResultNodesInfo();

ExecutionContext(ExecutionContextBuilder builder) {
this.graphQLSchema = builder.graphQLSchema;
Expand Down Expand Up @@ -282,4 +283,8 @@ public ExecutionContext transform(Consumer<ExecutionContextBuilder> builderConsu
builderConsumer.accept(builder);
return builder.build();
}

public ResultNodesInfo getResultNodesInfo() {
return resultNodesInfo;
}
}
29 changes: 28 additions & 1 deletion src/main/java/graphql/execution/ExecutionStrategy.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import static graphql.execution.FieldValueInfo.CompleteValueType.NULL;
import static graphql.execution.FieldValueInfo.CompleteValueType.OBJECT;
import static graphql.execution.FieldValueInfo.CompleteValueType.SCALAR;
import static graphql.execution.ResultNodesInfo.MAX_RESULT_NODES;
import static graphql.execution.instrumentation.SimpleInstrumentationContext.nonNullCtx;
import static graphql.schema.DataFetchingEnvironmentImpl.newDataFetchingEnvironment;
import static graphql.schema.GraphQLTypeUtil.isEnum;
Expand Down Expand Up @@ -239,7 +240,23 @@ protected CompletableFuture<FetchedValue> fetchField(ExecutionContext executionC
MergedField field = parameters.getField();
GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType();
GraphQLFieldDefinition fieldDef = getFieldDef(executionContext.getGraphQLSchema(), parentType, field.getSingleField());
GraphQLCodeRegistry codeRegistry = executionContext.getGraphQLSchema().getCodeRegistry();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is moved down in this PR

In latest master we added one extra method to break up the logic here. The code registry is used in the method being called here

return fetchField(fieldDef, executionContext, parameters);
}

private CompletableFuture<FetchedValue> fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext executionContext, ExecutionStrategyParameters parameters) {

int resultNodesCount = executionContext.getResultNodesInfo().incrementAndGetResultNodesCount();

Integer maxNodes;
if ((maxNodes = executionContext.getGraphQLContext().get(MAX_RESULT_NODES)) != null) {
if (resultNodesCount > maxNodes) {
executionContext.getResultNodesInfo().maxResultNodesExceeded();
return CompletableFuture.completedFuture(new FetchedValue(null, null, ImmutableKit.emptyList(), null));
Copy link
Member Author

@dondonz dondonz Mar 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FetchedValue has a slightly different constructor in v21 (compared to master), however this line still means effectively the same thing (return early with null)

}
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The next two lines (variables field and parentType) are not part of max result backport. I have done this to make this method look as close as possible to master

MergedField field = parameters.getField();
GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType();

// if the DF (like PropertyDataFetcher) does not use the arguments or execution step info then dont build any

Expand Down Expand Up @@ -274,6 +291,7 @@ protected CompletableFuture<FetchedValue> fetchField(ExecutionContext executionC
.queryDirectives(queryDirectives)
.build();
});
GraphQLCodeRegistry codeRegistry = executionContext.getGraphQLSchema().getCodeRegistry();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's where the codeRegistry line got moved to

DataFetcher<?> dataFetcher = codeRegistry.getDataFetcher(parentType, fieldDef);

Instrumentation instrumentation = executionContext.getInstrumentation();
Expand Down Expand Up @@ -568,6 +586,15 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext,
List<FieldValueInfo> fieldValueInfos = new ArrayList<>(size.orElse(1));
int index = 0;
for (Object item : iterableValues) {
int resultNodesCount = executionContext.getResultNodesInfo().incrementAndGetResultNodesCount();
Integer maxNodes;
if ((maxNodes = executionContext.getGraphQLContext().get(MAX_RESULT_NODES)) != null) {
if (resultNodesCount > maxNodes) {
executionContext.getResultNodesInfo().maxResultNodesExceeded();
return new FieldValueInfo(NULL, completedFuture(ExecutionResult.newExecutionResult().build()), fieldValueInfos);
Copy link
Member Author

@dondonz dondonz Mar 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In master this second argument is completedFuture(null). In this version it MUST be completedFuture(ExecutionResult.newExecutionResult().build())

This is because since 21.x was last released we have made a big change to remove unnecessary ExecutionResult objects. My test hung weirdly because in 21.x land, the engine never expects a null, it expects an ExecutionResult

}
}

ResultPath indexedPath = parameters.getPath().segment(index);

ExecutionStepInfo stepInfoForListElement = executionStepInfoFactory.newExecutionStepInfoForListElement(executionStepInfo, indexedPath);
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/graphql/execution/FetchedValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class FetchedValue {
private final Object localContext;
private final ImmutableList<GraphQLError> errors;

private FetchedValue(Object fetchedValue, Object rawFetchedValue, ImmutableList<GraphQLError> errors, Object localContext) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has default visibility on current master

FetchedValue(Object fetchedValue, Object rawFetchedValue, ImmutableList<GraphQLError> errors, Object localContext) {
this.fetchedValue = fetchedValue;
this.rawFetchedValue = rawFetchedValue;
this.errors = errors;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/graphql/execution/FieldValueInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public enum CompleteValueType {
private final CompletableFuture<ExecutionResult> fieldValue;
private final List<FieldValueInfo> fieldValueInfos;

private FieldValueInfo(CompleteValueType completeValueType, CompletableFuture<ExecutionResult> fieldValue, List<FieldValueInfo> fieldValueInfos) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has default visibility on current master

FieldValueInfo(CompleteValueType completeValueType, CompletableFuture<ExecutionResult> fieldValue, List<FieldValueInfo> fieldValueInfos) {
assertNotNull(fieldValueInfos, () -> "fieldValueInfos can't be null");
this.completeValueType = completeValueType;
this.fieldValue = fieldValue;
Expand Down
55 changes: 55 additions & 0 deletions src/main/java/graphql/execution/ResultNodesInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package graphql.execution;

import graphql.Internal;
import graphql.PublicApi;

import java.util.concurrent.atomic.AtomicInteger;

/**
* This class is used to track the number of result nodes that have been created during execution.
* After each execution the GraphQLContext contains a ResultNodeInfo object under the key {@link ResultNodesInfo#RESULT_NODES_INFO}
* <p>
* The number of result can be limited (and should be for security reasons) by setting the maximum number of result nodes
* in the GraphQLContext under the key {@link ResultNodesInfo#MAX_RESULT_NODES} to an Integer
* </p>
*/
@PublicApi
public class ResultNodesInfo {

public static final String MAX_RESULT_NODES = "__MAX_RESULT_NODES";
public static final String RESULT_NODES_INFO = "__RESULT_NODES_INFO";

private volatile boolean maxResultNodesExceeded = false;
private final AtomicInteger resultNodesCount = new AtomicInteger(0);

@Internal
public int incrementAndGetResultNodesCount() {
return resultNodesCount.incrementAndGet();
}

@Internal
public void maxResultNodesExceeded() {
this.maxResultNodesExceeded = true;
}

/**
* The number of result nodes created.
* Note: this can be higher than max result nodes because
* a each node that exceeds the number of max nodes is set to null,
* but still is a result node (with value null)
*
* @return number of result nodes created
*/
public int getResultNodesCount() {
return resultNodesCount.get();
}

/**
* If the number of result nodes has exceeded the maximum allowed numbers.
*
* @return true if the number of result nodes has exceeded the maximum allowed numbers
*/
public boolean isMaxResultNodesExceeded() {
return maxResultNodesExceeded;
}
}
141 changes: 141 additions & 0 deletions src/test/groovy/graphql/GraphQLTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import graphql.execution.ExecutionId
import graphql.execution.ExecutionIdProvider
import graphql.execution.ExecutionStrategyParameters
import graphql.execution.MissingRootTypeException
import graphql.execution.ResultNodesInfo
import graphql.execution.SubscriptionExecutionStrategy
import graphql.execution.ValueUnboxer
import graphql.execution.instrumentation.ChainedInstrumentation
Expand Down Expand Up @@ -49,6 +50,7 @@ import static graphql.ExecutionInput.Builder
import static graphql.ExecutionInput.newExecutionInput
import static graphql.Scalars.GraphQLInt
import static graphql.Scalars.GraphQLString
import static graphql.execution.ResultNodesInfo.MAX_RESULT_NODES
import static graphql.schema.GraphQLArgument.newArgument
import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition
import static graphql.schema.GraphQLInputObjectField.newInputObjectField
Expand Down Expand Up @@ -1440,4 +1442,143 @@ many lines''']
then:
!er.errors.isEmpty()
}

def "max result nodes not breached"() {
given:
def sdl = '''

type Query {
hello: String
}
'''
def df = { env -> "world" } as DataFetcher
def fetchers = ["Query": ["hello": df]]
def schema = TestUtil.schema(sdl, fetchers)
def graphQL = GraphQL.newGraphQL(schema).build()

def query = "{ hello h1: hello h2: hello h3: hello } "
def ei = newExecutionInput(query).build()
ei.getGraphQLContext().put(MAX_RESULT_NODES, 4);

when:
def er = graphQL.execute(ei)
def rni = ei.getGraphQLContext().get(ResultNodesInfo.RESULT_NODES_INFO) as ResultNodesInfo
then:
!rni.maxResultNodesExceeded
rni.resultNodesCount == 4
er.data == [hello: "world", h1: "world", h2: "world", h3: "world"]
}

def "max result nodes breached"() {
given:
def sdl = '''

type Query {
hello: String
}
'''
def df = { env -> "world" } as DataFetcher
def fetchers = ["Query": ["hello": df]]
def schema = TestUtil.schema(sdl, fetchers)
def graphQL = GraphQL.newGraphQL(schema).build()

def query = "{ hello h1: hello h2: hello h3: hello } "
def ei = newExecutionInput(query).build()
ei.getGraphQLContext().put(MAX_RESULT_NODES, 3);

when:
def er = graphQL.execute(ei)
def rni = ei.getGraphQLContext().get(ResultNodesInfo.RESULT_NODES_INFO) as ResultNodesInfo
then:
rni.maxResultNodesExceeded
rni.resultNodesCount == 4
er.data == [hello: "world", h1: "world", h2: "world", h3: null]
}

def "max result nodes breached with list"() {
given:
def sdl = '''

type Query {
hello: [String]
}
'''
def df = { env -> ["w1", "w2", "w3"] } as DataFetcher
def fetchers = ["Query": ["hello": df]]
def schema = TestUtil.schema(sdl, fetchers)
def graphQL = GraphQL.newGraphQL(schema).build()

def query = "{ hello}"
def ei = newExecutionInput(query).build()
ei.getGraphQLContext().put(MAX_RESULT_NODES, 3);

when:
def er = graphQL.execute(ei)
def rni = ei.getGraphQLContext().get(ResultNodesInfo.RESULT_NODES_INFO) as ResultNodesInfo
then:
rni.maxResultNodesExceeded
rni.resultNodesCount == 4
er.data == [hello: null]
}

def "max result nodes breached with list 2"() {
given:
def sdl = '''

type Query {
hello: [Foo]
}
type Foo {
name: String
}
'''
def df = { env -> [[name: "w1"], [name: "w2"], [name: "w3"]] } as DataFetcher
def fetchers = ["Query": ["hello": df]]
def schema = TestUtil.schema(sdl, fetchers)
def graphQL = GraphQL.newGraphQL(schema).build()

def query = "{ hello {name}}"
def ei = newExecutionInput(query).build()
// we have 7 result nodes overall
ei.getGraphQLContext().put(MAX_RESULT_NODES, 6);

when:
def er = graphQL.execute(ei)
def rni = ei.getGraphQLContext().get(ResultNodesInfo.RESULT_NODES_INFO) as ResultNodesInfo
then:
rni.resultNodesCount == 7
rni.maxResultNodesExceeded
er.data == [hello: [[name: "w1"], [name: "w2"], [name: null]]]
}

def "max result nodes not breached with list"() {
given:
def sdl = '''

type Query {
hello: [Foo]
}
type Foo {
name: String
}
'''
def df = { env -> [[name: "w1"], [name: "w2"], [name: "w3"]] } as DataFetcher
def fetchers = ["Query": ["hello": df]]
def schema = TestUtil.schema(sdl, fetchers)
def graphQL = GraphQL.newGraphQL(schema).build()

def query = "{ hello {name}}"
def ei = newExecutionInput(query).build()
// we have 7 result nodes overall
ei.getGraphQLContext().put(MAX_RESULT_NODES, 7);

when:
def er = graphQL.execute(ei)
def rni = ei.getGraphQLContext().get(ResultNodesInfo.RESULT_NODES_INFO) as ResultNodesInfo
then:
!rni.maxResultNodesExceeded
rni.resultNodesCount == 7
er.data == [hello: [[name: "w1"], [name: "w2"], [name: "w3"]]]
}

}