Skip to content
Draft
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
42 changes: 0 additions & 42 deletions src/main/java/graphql/Directives.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@
import static graphql.introspection.Introspection.DirectiveLocation.INLINE_FRAGMENT;
import static graphql.introspection.Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION;
import static graphql.introspection.Introspection.DirectiveLocation.INPUT_OBJECT;
import static graphql.introspection.Introspection.DirectiveLocation.MUTATION;
import static graphql.introspection.Introspection.DirectiveLocation.QUERY;
import static graphql.introspection.Introspection.DirectiveLocation.SCALAR;
import static graphql.introspection.Introspection.DirectiveLocation.SUBSCRIPTION;
import static graphql.language.DirectiveLocation.newDirectiveLocation;
import static graphql.language.InputValueDefinition.newInputValueDefinition;
import static graphql.language.NonNullType.newNonNullType;
Expand All @@ -49,7 +46,6 @@ public class Directives {
private static final String SPECIFIED_BY = "specifiedBy";
private static final String ONE_OF = "oneOf";
private static final String DEFER = "defer";
private static final String EXPERIMENTAL_DISABLE_ERROR_PROPAGATION = "experimental_disableErrorPropagation";

public static final DirectiveDefinition DEPRECATED_DIRECTIVE_DEFINITION;
public static final DirectiveDefinition INCLUDE_DIRECTIVE_DEFINITION;
Expand All @@ -59,8 +55,6 @@ public class Directives {
public static final DirectiveDefinition ONE_OF_DIRECTIVE_DEFINITION;
@ExperimentalApi
public static final DirectiveDefinition DEFER_DIRECTIVE_DEFINITION;
@ExperimentalApi
public static final DirectiveDefinition EXPERIMENTAL_DISABLE_ERROR_PROPAGATION_DIRECTIVE_DEFINITION;

public static final String BOOLEAN = "Boolean";
public static final String STRING = "String";
Expand Down Expand Up @@ -148,13 +142,6 @@ public class Directives {
.type(newTypeName().name(STRING).build())
.build())
.build();
EXPERIMENTAL_DISABLE_ERROR_PROPAGATION_DIRECTIVE_DEFINITION = DirectiveDefinition.newDirectiveDefinition()
.name(EXPERIMENTAL_DISABLE_ERROR_PROPAGATION)
.directiveLocation(newDirectiveLocation().name(QUERY.name()).build())
.directiveLocation(newDirectiveLocation().name(MUTATION.name()).build())
.directiveLocation(newDirectiveLocation().name(SUBSCRIPTION.name()).build())
.description(createDescription("This directive allows returning null in non-null positions that have an associated error"))
.build();
}

/**
Expand Down Expand Up @@ -248,14 +235,6 @@ public class Directives {
.definition(ONE_OF_DIRECTIVE_DEFINITION)
.build();

@ExperimentalApi
public static final GraphQLDirective ExperimentalDisableErrorPropagationDirective = GraphQLDirective.newDirective()
.name(EXPERIMENTAL_DISABLE_ERROR_PROPAGATION)
.description("This directive disables error propagation when a non nullable field returns null for the given operation.")
.validLocations(QUERY, MUTATION, SUBSCRIPTION)
.definition(EXPERIMENTAL_DISABLE_ERROR_PROPAGATION_DIRECTIVE_DEFINITION)
.build();

/**
* The set of all built-in directives that are always present in a graphql schema.
* The iteration order is stable and meaningful.
Expand All @@ -275,7 +254,6 @@ public class Directives {
directives.add(SpecifiedByDirective);
directives.add(OneOfDirective);
directives.add(DeferDirective);
directives.add(ExperimentalDisableErrorPropagationDirective);
BUILT_IN_DIRECTIVES = Collections.unmodifiableSet(directives);

LinkedHashMap<String, GraphQLDirective> map = new LinkedHashMap<>();
Expand Down Expand Up @@ -310,24 +288,4 @@ public static boolean isBuiltInDirective(GraphQLDirective directive) {
private static Description createDescription(String s) {
return new Description(s, null, false);
}

private static final AtomicBoolean EXPERIMENTAL_DISABLE_ERROR_PROPAGATION_DIRECTIVE_ENABLED = new AtomicBoolean(true);

/**
* This can be used to get the state the `@experimental_disableErrorPropagation` directive support on a JVM wide basis .
* @return true if the `@experimental_disableErrorPropagation` directive will be respected
*/
public static boolean isExperimentalDisableErrorPropagationDirectiveEnabled() {
return EXPERIMENTAL_DISABLE_ERROR_PROPAGATION_DIRECTIVE_ENABLED.get();
}

/**
* This can be used to disable the `@experimental_disableErrorPropagation` directive support on a JVM wide basis in case your server
* implementation does NOT want to act on this directive ever.
*
* @param flag the desired state of the flag
*/
public static void setExperimentalDisableErrorPropagationEnabled(boolean flag) {
EXPERIMENTAL_DISABLE_ERROR_PROPAGATION_DIRECTIVE_ENABLED.set(flag);
}
}
17 changes: 16 additions & 1 deletion src/main/java/graphql/ExecutionInput.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import graphql.collect.ImmutableKit;
import graphql.execution.ExecutionId;
import graphql.execution.OnError;
import graphql.execution.RawVariables;
import graphql.execution.preparsed.persisted.PersistedQuerySupport;
import org.dataloader.DataLoaderRegistry;
Expand Down Expand Up @@ -36,6 +37,7 @@ public class ExecutionInput {
private final Locale locale;
private final AtomicBoolean cancelled;
private final boolean profileExecution;
private final OnError onError;

/**
* In order for {@link #getQuery()} to never be null, use this to mark
Expand All @@ -62,6 +64,7 @@ private ExecutionInput(Builder builder) {
this.extensions = builder.extensions;
this.cancelled = builder.cancelled;
this.profileExecution = builder.profileExecution;
this.onError = builder.onError;
}

private static String assertQuery(Builder builder) {
Expand Down Expand Up @@ -227,6 +230,10 @@ public boolean isProfileExecution() {
return profileExecution;
}

public OnError getOnError() {
return onError;
}

/**
* This helps you transform the current ExecutionInput object into another one by starting a builder with all
* the current values and allows you to transform it how you want.
Expand All @@ -248,7 +255,8 @@ public ExecutionInput transform(Consumer<Builder> builderConsumer) {
.variables(this.rawVariables.toMap())
.extensions(this.extensions)
.executionId(this.executionId)
.locale(this.locale);
.locale(this.locale)
.onError(this.onError);

builderConsumer.accept(builder);

Expand All @@ -267,6 +275,7 @@ public String toString() {
", dataLoaderRegistry=" + dataLoaderRegistry +
", executionId= " + executionId +
", locale= " + locale +
", onError= " + onError +
'}';
}

Expand Down Expand Up @@ -308,6 +317,7 @@ public static class Builder {
private ExecutionId executionId;
private AtomicBoolean cancelled = new AtomicBoolean(false);
private boolean profileExecution;
private OnError onError = OnError.PROPAGATE;

/**
* Package level access to the graphql context
Expand Down Expand Up @@ -461,6 +471,11 @@ public Builder profileExecution(boolean profileExecution) {
return this;
}

public Builder onError(OnError onError) {
this.onError = onError;
return this;
}

public ExecutionInput build() {
return new ExecutionInput(this);
}
Expand Down
34 changes: 22 additions & 12 deletions src/main/java/graphql/execution/Execution.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@


import com.google.common.collect.ImmutableList;
import graphql.Directives;
import graphql.EngineRunningState;
import graphql.ExecutionInput;
import graphql.ExecutionResult;
Expand All @@ -29,7 +28,6 @@
import graphql.extensions.ExtensionsBuilder;
import graphql.incremental.DelayedIncrementalPartialResult;
import graphql.incremental.IncrementalExecutionResultImpl;
import graphql.language.Directive;
import graphql.language.Document;
import graphql.language.NodeUtil;
import graphql.language.OperationDefinition;
Expand All @@ -46,9 +44,9 @@
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;

import static graphql.Directives.EXPERIMENTAL_DISABLE_ERROR_PROPAGATION_DIRECTIVE_DEFINITION;
import static graphql.execution.ExecutionContextBuilder.newExecutionContextBuilder;
import static graphql.execution.ExecutionStepInfo.newExecutionStepInfo;
import static graphql.execution.ExecutionStrategyParameters.newParameters;
Expand Down Expand Up @@ -103,7 +101,7 @@ public CompletableFuture<ExecutionResult> execute(Document document, GraphQLSche
return completedFuture(abortExecutionException.toExecutionResult());
}

boolean propagateErrorsOnNonNullContractFailure = propagateErrorsOnNonNullContractFailure(getOperationResult.operationDefinition.getDirectives());
OnError onError = isExperimentalOnErrorEnabled()? executionInput.getOnError() : OnError.PROPAGATE;

GraphQLContext graphQLContext = executionInput.getGraphQLContext();
Locale locale = executionInput.getLocale();
Expand Down Expand Up @@ -137,7 +135,7 @@ public CompletableFuture<ExecutionResult> execute(Document document, GraphQLSche
.valueUnboxer(valueUnboxer)
.responseMapFactory(responseMapFactory)
.executionInput(executionInput)
.propagapropagateErrorsOnNonNullContractFailureeErrors(propagateErrorsOnNonNullContractFailure)
.onError(onError)
.engineRunningState(engineRunningState)
.profiler(profiler)
.build();
Expand Down Expand Up @@ -317,12 +315,24 @@ private ExecutionResult mergeExtensionsBuilderIfPresent(ExecutionResult executio
return executionResult;
}

private boolean propagateErrorsOnNonNullContractFailure(List<Directive> directives) {
boolean jvmWideEnabled = Directives.isExperimentalDisableErrorPropagationDirectiveEnabled();
if (!jvmWideEnabled) {
return true;
}
Directive foundDirective = NodeUtil.findNodeByName(directives, EXPERIMENTAL_DISABLE_ERROR_PROPAGATION_DIRECTIVE_DEFINITION.getName());
return foundDirective == null;
private static final AtomicBoolean EXPERIMENTAL_ON_ERROR_ENABLED = new AtomicBoolean(true);

/**
* This can be used to get the state the `onError` request parameter support on a JVM-wide basis.
* @return true if the `onError` request parameter will be respected
*/
public static boolean isExperimentalOnErrorEnabled() {
return EXPERIMENTAL_ON_ERROR_ENABLED.get();
}

/**
* This can be used to disable the `onError` request parameter support on a JVM-wide basis in case your server
* implementation does NOT want to act on the request parameter ever.
*
* @param flag the desired state of the flag
*/
public static void setExperimentalOnErrorEnabled(boolean flag) {
EXPERIMENTAL_ON_ERROR_ENABLED.set(flag);
}

}
14 changes: 7 additions & 7 deletions src/main/java/graphql/execution/ExecutionContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public class ExecutionContext {

private final ExecutionInput executionInput;
private final Supplier<ExecutableNormalizedOperation> queryTree;
private final boolean propagateErrorsOnNonNullContractFailure;
private final OnError onError;

// this is modified after creation so it needs to be volatile to ensure visibility across Threads
private volatile DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP;
Expand Down Expand Up @@ -108,7 +108,7 @@ public class ExecutionContext {
this.localContext = builder.localContext;
this.executionInput = builder.executionInput;
this.dataLoaderDispatcherStrategy = builder.dataLoaderDispatcherStrategy;
this.propagateErrorsOnNonNullContractFailure = builder.propagateErrorsOnNonNullContractFailure;
this.onError = builder.onError;
this.engineRunningState = builder.engineRunningState;
this.profiler = builder.profiler;
// lazy loading for performance
Expand Down Expand Up @@ -233,15 +233,15 @@ public ValueUnboxer getValueUnboxer() {
}

/**
* @return true if the current operation should propagate errors in non-null positions
* @return the [OnError] behavior requested by the client.
* Propagating errors is the default. Error aware clients may opt in returning null in non-null positions
* by using the `@experimental_disableErrorPropagation` directive.
* or halting the execution.
*
* @see graphql.Directives#setExperimentalDisableErrorPropagationEnabled(boolean) to change the JVM wide default
* @see Execution#setExperimentalOnErrorEnabled(boolean) (boolean) to change the JVM wide default
*/
@ExperimentalApi
public boolean propagateErrorsOnNonNullContractFailure() {
return propagateErrorsOnNonNullContractFailure;
public OnError getOnError() {
return onError;
}

/**
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/graphql/execution/ExecutionContextBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public class ExecutionContextBuilder {
Object localContext;
ExecutionInput executionInput;
DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP;
boolean propagateErrorsOnNonNullContractFailure = true;
OnError onError = OnError.PROPAGATE;
EngineRunningState engineRunningState;
ResponseMapFactory responseMapFactory = ResponseMapFactory.DEFAULT;
Profiler profiler;
Expand Down Expand Up @@ -104,7 +104,7 @@ public ExecutionContextBuilder() {
valueUnboxer = other.getValueUnboxer();
executionInput = other.getExecutionInput();
dataLoaderDispatcherStrategy = other.getDataLoaderDispatcherStrategy();
propagateErrorsOnNonNullContractFailure = other.propagateErrorsOnNonNullContractFailure();
onError = other.getOnError();
engineRunningState = other.getEngineRunningState();
responseMapFactory = other.getResponseMapFactory();
profiler = other.getProfiler();
Expand Down Expand Up @@ -245,8 +245,8 @@ public ExecutionContextBuilder resetErrors() {
}

@ExperimentalApi
public ExecutionContextBuilder propagapropagateErrorsOnNonNullContractFailureeErrors(boolean propagateErrorsOnNonNullContractFailure) {
this.propagateErrorsOnNonNullContractFailure = propagateErrorsOnNonNullContractFailure;
public ExecutionContextBuilder onError(OnError onError) {
this.onError = onError;
return this;
}

Expand Down
13 changes: 9 additions & 4 deletions src/main/java/graphql/execution/NonNullableFieldValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
/**
* This will check that a value is non-null when the type definition says it must be and, it will throw {@link NonNullableFieldWasNullException}
* if this is not the case.
*
* See: https://spec.graphql.org/October2021/#sec-Errors-and-Non-Nullability
* <p>
* See: <a href="https://spec.graphql.org/October2021/#sec-Errors-and-Non-Nullability">https://spec.graphql.org/October2021/#sec-Errors-and-Non-Nullability</a>
*/
@Internal
public class NonNullableFieldValidator {
Expand All @@ -20,7 +20,7 @@ public NonNullableFieldValidator(ExecutionContext executionContext) {
}

/**
* Called to check that a value is non-null if the type requires it to be non null
* Called to check that a value is non-null if the type requires it to be non-null
*
* @param parameters the execution strategy parameters
* @param result the result to check
Expand Down Expand Up @@ -55,8 +55,13 @@ public <T> T validate(ExecutionStrategyParameters parameters, T result) throws N
} else {
executionContext.addError(error, path);
}
if (executionContext.propagateErrorsOnNonNullContractFailure()) {

OnError onError = executionContext.getOnError();
if (onError == OnError.PROPAGATE) {

throw nonNullException;
} else if (onError == OnError.HALT) {
throw new AbortExecutionException(executionContext.getErrors());
}
}
}
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/graphql/execution/OnError.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package graphql.execution;

import graphql.ExperimentalApi;
import org.jspecify.annotations.NullMarked;

/**
* Controls how errors are handled during execution
*/
@ExperimentalApi
@NullMarked
public enum OnError {
NULL,
PROPAGATE,
HALT
}
3 changes: 0 additions & 3 deletions src/test/groovy/graphql/Issue2141.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ directive @deprecated(
reason: String! = "No longer supported"
) on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION
"This directive disables error propagation when a non nullable field returns null for the given operation."
directive @experimental_disableErrorPropagation on QUERY | MUTATION | SUBSCRIPTION
"Directs the executor to include this field or fragment only when the `if` argument is true"
directive @include(
"Included when true."
Expand Down
2 changes: 1 addition & 1 deletion src/test/groovy/graphql/StarWarsIntrospectionTests.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,6 @@ class StarWarsIntrospectionTests extends Specification {
schemaParts.get('mutationType').size() == 1
schemaParts.get('subscriptionType') == null
schemaParts.get('types').size() == 17
schemaParts.get('directives').size() == 7
schemaParts.get('directives').size() == 6
}
}
Loading