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
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using HotChocolate.Language;

namespace HotChocolate.Execution.Options;

Expand All @@ -14,4 +15,15 @@ public interface IErrorHandlerOptionsAccessor
/// <see cref="Debugger.IsAttached"/>.
/// </summary>
bool IncludeExceptionDetails { get; }

/// <summary>
/// Gets the default error handling mode for null propagation.
/// </summary>
ErrorHandlingMode DefaultErrorHandlingMode { get; }

/// <summary>
/// Gets a value indicating whether the <see cref="DefaultErrorHandlingMode"/> can be
/// overridden on a per-request basis.
/// </summary>
bool AllowErrorHandlingModeOverride { get; }
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using HotChocolate.Language;
using HotChocolate.PersistedOperations;

namespace HotChocolate.Execution.Options;
Expand Down Expand Up @@ -67,4 +68,17 @@ public PersistedOperationOptions PersistedOperations
field = value;
}
} = new();

/// <summary>
/// Gets or sets the default error handling mode for null propagation.
/// <see cref="ErrorHandlingMode.Propagate"/> by default.
/// </summary>
public ErrorHandlingMode DefaultErrorHandlingMode { get; set; } = ErrorHandlingMode.Propagate;

/// <summary>
/// Gets or sets whether the <see cref="DefaultErrorHandlingMode"/> can be overridden
/// on a per-request basis.
/// <c>false</c> by default.
/// </summary>
public bool AllowErrorHandlingModeOverride { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Runtime.CompilerServices;
using HotChocolate.Execution.DependencyInjection;
using HotChocolate.Execution.Instrumentation;
using HotChocolate.Execution.Options;
using HotChocolate.Execution.Processing.Tasks;
using HotChocolate.Fetching;
using HotChocolate.Resolvers;
Expand Down Expand Up @@ -91,8 +92,12 @@ public void Initialize(
_batchDispatcher = batchDispatcher;
_variableIndex = variableIndex;

var errorHandlingMode = _requestContext.Request.ErrorHandlingMode
?? _schema.GetOptions().DefaultErrorHandlingMode;
var executorOptions = _schema.Services.GetRequiredService<IRequestExecutorOptionsAccessor>();
var errorHandlingMode =
executorOptions.AllowErrorHandlingModeOverride
&& _requestContext.Request.ErrorHandlingMode is { } requestedMode
? requestedMode
: executorOptions.DefaultErrorHandlingMode;
_propagateNullValues = errorHandlingMode is Language.ErrorHandlingMode.Propagate;

IncludeFlags = operation.CreateIncludeFlags(variables);
Expand Down
6 changes: 0 additions & 6 deletions src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.Reflection;
using HotChocolate.Configuration;
using HotChocolate.Execution;
using HotChocolate.Language;
using HotChocolate.Types;

namespace HotChocolate;
Expand Down Expand Up @@ -232,9 +231,4 @@ public interface IReadOnlySchemaOptions
/// Applies the @serializeAs directive to scalar types that specify a serialization format.
/// </summary>
bool ApplySerializeAsToScalars { get; }

/// <summary>
/// Gets the default error handling mode for null propagation.
/// </summary>
ErrorHandlingMode DefaultErrorHandlingMode { get; }
}
7 changes: 1 addition & 6 deletions src/HotChocolate/Core/src/Types/SchemaOptions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.Reflection;
using HotChocolate.Configuration;
using HotChocolate.Execution;
using HotChocolate.Language;
using HotChocolate.Types;

namespace HotChocolate;
Expand Down Expand Up @@ -190,9 +189,6 @@ public int OperationDocumentCacheSize
/// <inheritdoc cref="IReadOnlySchemaOptions.ApplySerializeAsToScalars"/>
public bool ApplySerializeAsToScalars { get; set; }

/// <inheritdoc cref="IReadOnlySchemaOptions.DefaultErrorHandlingMode"/>
public ErrorHandlingMode DefaultErrorHandlingMode { get; set; } = ErrorHandlingMode.Propagate;

/// <summary>
/// Creates a mutable options object from a read-only options object.
/// </summary>
Expand Down Expand Up @@ -234,7 +230,6 @@ public static SchemaOptions FromOptions(IReadOnlySchemaOptions options)
ApplyShareableToPageInfo = options.ApplyShareableToPageInfo,
ApplyShareableToConnections = options.ApplyShareableToConnections,
ApplyShareableToNodeFields = options.ApplyShareableToNodeFields,
ApplySerializeAsToScalars = options.ApplySerializeAsToScalars,
DefaultErrorHandlingMode = options.DefaultErrorHandlingMode
ApplySerializeAsToScalars = options.ApplySerializeAsToScalars
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -250,15 +250,15 @@ public async Task List_NonNullElementHasError_NullMode(string fieldType)
}

[Fact]
public async Task DefaultErrorHandlingMode_AppliesFromSchemaOptions()
public async Task DefaultErrorHandlingMode_AppliesFromRequestExecutorOptions()
{
// arrange
using var snapshot = SnapshotHelpers.StartResultSnapshot();

var executor = await new ServiceCollection()
.AddGraphQL()
.AddDocumentFromString(SchemaText)
.ModifyOptions(o => o.DefaultErrorHandlingMode = ErrorHandlingMode.Null)
.ModifyRequestOptions(o => o.DefaultErrorHandlingMode = ErrorHandlingMode.Null)
.AddResolver("Query", "foo", _ => new(new object()))
.AddResolver("Foo", "nullable_list_nullable_element", _ => new(new[] { new object() }))
.AddResolver("Foo", "nonnull_list_nullable_element", _ => new(new[] { new object() }))
Expand All @@ -285,12 +285,12 @@ public async Task DefaultErrorHandlingMode_AppliesFromSchemaOptions()
}

[Fact]
public async Task PerRequestOverride_OverridesSchemaDefault()
public async Task PerRequestOverride_OverridesDefaultErrorHandlingMode()
{
// arrange
using var snapshot = SnapshotHelpers.StartResultSnapshot();

// Server configured with Propagate (default), but request specifies Null
// Request executor options use Propagate (default), but the request specifies Null.
var executor = await CreateExecutorAsync();

var request =
Expand All @@ -307,6 +307,43 @@ public async Task PerRequestOverride_OverridesSchemaDefault()
snapshot.Add(result);
}

[Fact]
public async Task PerRequestOverride_IsIgnored_When_AllowErrorHandlingModeOverride_IsDisabled()
{
// arrange
using var snapshot = SnapshotHelpers.StartResultSnapshot();

// Request executor options use Propagate (default) and override disabled (default).
// Even though the request asks for Null, the configured DefaultErrorHandlingMode must win.
var executor = await new ServiceCollection()
.AddGraphQL()
.AddDocumentFromString(SchemaText)
.AddResolver("Query", "foo", _ => new(new object()))
.AddResolver("Foo", "nullable_list_nullable_element", _ => new(new[] { new object() }))
.AddResolver("Foo", "nonnull_list_nullable_element", _ => new(new[] { new object() }))
.AddResolver("Foo", "nullable_list_nonnull_element", _ => new(new[] { new object() }))
.AddResolver("Foo", "nonnull_list_nonnull_element", _ => new(new[] { new object() }))
.AddResolver("Foo", "nonnull_prop", _ => new(new object()))
.AddResolver("Foo", "nullable_prop", _ => new(new object()))
.AddResolver("Bar", "a", c => new(c.GetGlobalStateOrDefault<string>("a")))
.AddResolver("Bar", "b", c => new(c.GetGlobalStateOrDefault<string>("b")))
.AddResolver("Bar", "c", _ => throw new GraphQLException("ERROR"))
.BuildRequestExecutorAsync();

var request =
OperationRequestBuilder.New()
.SetDocument("{ foo { nonnull_prop { b } } }")
.AddGlobalState("b", null)
.SetErrorHandlingMode(ErrorHandlingMode.Null)
.Build();

// act
var result = await executor.ExecuteAsync(request);

// assert
snapshot.Add(result);
}

private const string SchemaText =
"""
type Query {
Expand Down Expand Up @@ -334,6 +371,7 @@ private static async Task<IRequestExecutor> CreateExecutorAsync()
return await new ServiceCollection()
.AddGraphQL()
.AddDocumentFromString(SchemaText)
.ModifyRequestOptions(o => o.AllowErrorHandlingModeOverride = true)
.AddResolver("Query", "foo", _ => new(new object()))
.AddResolver("Foo", "nullable_list_nullable_element", _ => new(new[] { new object() }))
.AddResolver("Foo", "nonnull_list_nullable_element", _ => new(new[] { new object() }))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"errors": [
{
"message": "Cannot return null for non-nullable field.",
"path": [
"foo",
"nonnull_prop",
"b"
],
"extensions": {
"code": "HC0018"
}
}
],
"data": {
"foo": null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ internal static ErrorHandlingMode ErrorHandlingMode(
return errorHandlingMode;
}

return context.Schema.GetOptions().DefaultErrorHandlingMode;
return requestOptions.DefaultErrorHandlingMode;
}

internal static bool AllowErrorHandlingModeOverride(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using HotChocolate.Caching.Memory;
using HotChocolate.Execution.Relay;
using HotChocolate.Fusion.Types;
using HotChocolate.Language;

namespace HotChocolate.Fusion.Execution;

Expand Down Expand Up @@ -101,21 +100,6 @@ public int PathSegmentLocalPoolCapacity
}
} = 64;

/// <summary>
/// Gets or sets the default error handling mode.
/// <see cref="ErrorHandlingMode.Propagate"/> by default.
/// </summary>
public ErrorHandlingMode DefaultErrorHandlingMode
{
get;
set
{
ExpectMutableOptions();

field = value;
}
} = ErrorHandlingMode.Propagate;

/// <summary>
/// Gets or sets whether the request executor should be initialized lazily.
/// <c>false</c> by default.
Expand Down Expand Up @@ -214,7 +198,6 @@ public FusionOptions Clone()
OperationExecutionPlanCacheDiagnostics = OperationExecutionPlanCacheDiagnostics,
OperationDocumentCacheSize = OperationDocumentCacheSize,
PathSegmentLocalPoolCapacity = PathSegmentLocalPoolCapacity,
DefaultErrorHandlingMode = DefaultErrorHandlingMode,
LazyInitialization = LazyInitialization,
NodeIdSerializerFormat = NodeIdSerializerFormat,
ApplySerializeAsToScalars = ApplySerializeAsToScalars,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using HotChocolate.Language;
using HotChocolate.PersistedOperations;

namespace HotChocolate.Fusion.Execution;
Expand Down Expand Up @@ -42,7 +43,22 @@ public bool CollectOperationPlanTelemetry
}

/// <summary>
/// Gets or sets whether the <see cref="FusionOptions.DefaultErrorHandlingMode"/> can be overriden
/// Gets or sets the default error handling mode.
/// <see cref="ErrorHandlingMode.Propagate"/> by default.
/// </summary>
public ErrorHandlingMode DefaultErrorHandlingMode
{
get;
set
{
ExpectMutableOptions();

field = value;
}
} = ErrorHandlingMode.Propagate;

/// <summary>
/// Gets or sets whether the <see cref="DefaultErrorHandlingMode"/> can be overridden
/// on a per-request basis.
/// <c>false</c> by default.
/// </summary>
Expand Down Expand Up @@ -124,6 +140,7 @@ public FusionRequestOptions Clone()
{
ExecutionTimeout = ExecutionTimeout,
CollectOperationPlanTelemetry = CollectOperationPlanTelemetry,
DefaultErrorHandlingMode = DefaultErrorHandlingMode,
AllowErrorHandlingModeOverride = AllowErrorHandlingModeOverride,
AllowOperationPlanRequests = AllowOperationPlanRequests,
PersistedOperations = PersistedOperations,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public async Task OnError_SchemaDefault_Null_AppliesWithoutPerRequestOverride()
("A", server1)
],
configureGatewayBuilder: builder =>
builder.ModifyOptions(o => o.DefaultErrorHandlingMode = ErrorHandlingMode.Null));
builder.ModifyRequestOptions(o => o.DefaultErrorHandlingMode = ErrorHandlingMode.Null));

// act — no per-request onError override
using var client = GraphQLHttpClient.Create(gateway.CreateClient());
Expand All @@ -83,6 +83,54 @@ public async Task OnError_SchemaDefault_Null_AppliesWithoutPerRequestOverride()
await MatchSnapshotAsync(gateway, request, result);
}

[Fact]
public async Task OnError_PerRequestOverride_IsIgnored_When_AllowErrorHandlingModeOverride_IsDisabled()
{
// arrange
using var server1 = CreateSourceSchema(
"A",
b => b.AddQueryType<SourceSchema1.Query>());

using var server2 = CreateSourceSchema(
"B",
b => b.AddQueryType<SourceSchema3.Query>());

using var gateway = await CreateCompositeSchemaAsync(
[
("A", server1),
("B", server2)
],
configureGatewayBuilder: builder =>
builder.ModifyRequestOptions(o =>
{
o.DefaultErrorHandlingMode = ErrorHandlingMode.Propagate;
o.AllowErrorHandlingModeOverride = false;
}));

// act
// Even though the request asks for Null, the gateway must ignore the override
// and apply the configured Propagate mode (so data is fully omitted).
using var client = GraphQLHttpClient.Create(gateway.CreateClient());

var request = new OperationRequest(
"""
{
topProduct {
price
name
}
}
""",
onError: ErrorHandlingMode.Null);

using var result = await client.PostAsync(
request,
new Uri("http://localhost:5000/graphql"));

// assert
await MatchSnapshotAsync(gateway, request, result);
}

[Fact]
public async Task OnError_Null_OnSourceSchema_Forwards_To_Subgraph_Request()
{
Expand Down
Loading
Loading