diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultHttpRequestInterceptor.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultHttpRequestInterceptor.cs
index 0f3701f7338..7928bb943fb 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultHttpRequestInterceptor.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultHttpRequestInterceptor.cs
@@ -34,6 +34,11 @@ public virtual ValueTask OnCreateAsync(
requestBuilder.TryAddGlobalState(WellKnownContextData.IncludeQueryPlan, true);
}
+ if (context.IsNullBubblingDisabled())
+ {
+ requestBuilder.TryAddGlobalState(WellKnownContextData.DisableNullBubbling, true);
+ }
+
return default;
}
}
diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultSocketSessionInterceptor.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultSocketSessionInterceptor.cs
index 486f0556c7a..9d746f40c81 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultSocketSessionInterceptor.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultSocketSessionInterceptor.cs
@@ -44,6 +44,11 @@ public virtual ValueTask OnRequestAsync(
requestBuilder.TryAddGlobalState(IncludeQueryPlan, true);
}
+ if (context.IsNullBubblingDisabled())
+ {
+ requestBuilder.TryAddGlobalState(WellKnownContextData.DisableNullBubbling, true);
+ }
+
return default;
}
diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HttpContextExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HttpContextExtensions.cs
index 62db2dbdfce..314b1bc11ae 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HttpContextExtensions.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HttpContextExtensions.cs
@@ -35,4 +35,17 @@ public static bool IncludeQueryPlan(this HttpContext context)
return false;
}
+
+ public static bool IsNullBubblingDisabled(this HttpContext context)
+ {
+ var headers = context.Request.Headers;
+
+ if (headers.TryGetValue(HttpHeaderKeys.DisableNullBubbling, out var values) &&
+ values.Any(v => v == HttpHeaderValues.DisableNullBubbling))
+ {
+ return true;
+ }
+
+ return false;
+ }
}
diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderKeys.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderKeys.cs
index d946a1a0dee..ad9d2b55b2f 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderKeys.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderKeys.cs
@@ -11,4 +11,6 @@ internal static class HttpHeaderKeys
public const string CacheControl = "Cache-Control";
public const string Preflight = "GraphQL-Preflight";
+
+ public const string DisableNullBubbling = "GraphQL-Disable-NullBubbling";
}
diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderValues.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderValues.cs
index 02bf31125f3..5f29e488e62 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderValues.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderValues.cs
@@ -6,5 +6,7 @@ internal static class HttpHeaderValues
public const string IncludeQueryPlan = "1";
+ public const string DisableNullBubbling = "1";
+
public const string NoCache = "no-cache";
}
diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/HttpRequestHeadersExtensions.cs b/src/HotChocolate/AspNetCore/src/Transport.Http/HttpRequestHeadersExtensions.cs
index 74abc5616dd..1a13eada177 100644
--- a/src/HotChocolate/AspNetCore/src/Transport.Http/HttpRequestHeadersExtensions.cs
+++ b/src/HotChocolate/AspNetCore/src/Transport.Http/HttpRequestHeadersExtensions.cs
@@ -29,5 +29,28 @@ public static HttpRequestHeaders AddGraphQLPreflight(this HttpRequestHeaders hea
headers.Add("GraphQL-Preflight", "1");
return headers;
- }
-}
\ No newline at end of file
+ }
+
+ ///
+ /// Adds the GraphQL-Disable-NullBubbling header to the request.
+ ///
+ ///
+ /// The to add the header to.
+ ///
+ ///
+ /// Returns the for configuration chaining.
+ ///
+ ///
+ /// is .
+ ///
+ public static HttpRequestHeaders AddGraphQLDisableNullBubbling(this HttpRequestHeaders headers)
+ {
+ if (headers == null)
+ {
+ throw new ArgumentNullException(nameof(headers));
+ }
+
+ headers.Add("GraphQL-Disable-NullBubbling", "1");
+ return headers;
+ }
+}
diff --git a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs
index 421e741b84f..2a5ddd8ed82 100644
--- a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs
+++ b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs
@@ -169,11 +169,6 @@ public static class WellKnownContextData
///
public const string NodeResolver = "HotChocolate.Relay.Node.Resolver";
- ///
- /// The key to check if relay support is enabled.
- ///
- public const string IsRelaySupportEnabled = "HotChocolate.Relay.IsEnabled";
-
///
/// The key to check if the global identification spec is enabled.
///
@@ -274,17 +269,22 @@ public static class WellKnownContextData
/// The key to access the authorization allowed flag on the member context.
///
public const string AllowAnonymous = "HotChocolate.Authorization.AllowAnonymous";
-
+
///
/// The key to access the true nullability flag on the execution context.
///
public const string EnableTrueNullability = "HotChocolate.Types.EnableTrueNullability";
-
+
+ ///
+ /// Disables null-bubbling for the current request.
+ ///
+ public const string DisableNullBubbling = "HotChocolate.Execution.DisableNullBubbling";
+
///
/// The key to access the tag options object.
///
public const string TagOptions = "HotChocolate.Types.TagOptions";
-
+
///
/// Type key to access the internal schema options.
///
diff --git a/src/HotChocolate/Core/src/Execution/Options/IRequestExecutorOptionsAccessor.cs b/src/HotChocolate/Core/src/Execution/Options/IRequestExecutorOptionsAccessor.cs
index 1207b425206..2f7738e5557 100644
--- a/src/HotChocolate/Core/src/Execution/Options/IRequestExecutorOptionsAccessor.cs
+++ b/src/HotChocolate/Core/src/Execution/Options/IRequestExecutorOptionsAccessor.cs
@@ -8,6 +8,12 @@ namespace HotChocolate.Execution.Options;
///
public interface IRequestExecutorOptionsAccessor
: IErrorHandlerOptionsAccessor
- , IRequestTimeoutOptionsAccessor
- , IComplexityAnalyzerOptionsAccessor
- , IPersistedQueryOptionsAccessor;
+ , IRequestTimeoutOptionsAccessor
+ , IComplexityAnalyzerOptionsAccessor
+ , IPersistedQueryOptionsAccessor
+{
+ ///
+ /// Determine whether null-bubbling can be disabled on a per-request basis.
+ ///
+ bool AllowDisablingNullBubbling { get; }
+}
diff --git a/src/HotChocolate/Core/src/Execution/Options/RequestExecutorOptions.cs b/src/HotChocolate/Core/src/Execution/Options/RequestExecutorOptions.cs
index 229128b2361..b37dc655ead 100644
--- a/src/HotChocolate/Core/src/Execution/Options/RequestExecutorOptions.cs
+++ b/src/HotChocolate/Core/src/Execution/Options/RequestExecutorOptions.cs
@@ -72,9 +72,14 @@ public IError OnlyPersistedQueriesAreAllowedError
get => _onlyPersistedQueriesAreAllowedError;
set
{
- _onlyPersistedQueriesAreAllowedError = value
- ?? throw new ArgumentNullException(
+ _onlyPersistedQueriesAreAllowedError = value ??
+ throw new ArgumentNullException(
nameof(OnlyPersistedQueriesAreAllowedError));
}
}
+
+ ///
+ /// Determine whether null-bubbling can be disabled on a per-request basis.
+ ///
+ public bool AllowDisablingNullBubbling { get; set; } = false;
}
diff --git a/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs b/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs
index 257d98021e3..22513c4dee5 100644
--- a/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs
+++ b/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using HotChocolate.Execution.Options;
using HotChocolate.Execution.Processing;
using HotChocolate.Language;
using HotChocolate.Types;
@@ -19,6 +20,7 @@ internal sealed class OperationResolverMiddleware
{
private readonly RequestDelegate _next;
private readonly ObjectPool _operationCompilerPool;
+ private readonly IRequestExecutorOptionsAccessor _options;
private readonly VariableCoercionHelper _coercionHelper;
private readonly IReadOnlyList? _optimizers;
@@ -26,6 +28,7 @@ private OperationResolverMiddleware(
RequestDelegate next,
ObjectPool operationCompilerPool,
IEnumerable optimizers,
+ IRequestExecutorOptionsAccessor options,
VariableCoercionHelper coercionHelper)
{
if (optimizers is null)
@@ -37,6 +40,8 @@ private OperationResolverMiddleware(
throw new ArgumentNullException(nameof(next));
_operationCompilerPool = operationCompilerPool ??
throw new ArgumentNullException(nameof(operationCompilerPool));
+ _options = options ??
+ throw new ArgumentNullException(nameof(options));
_coercionHelper = coercionHelper ??
throw new ArgumentNullException(nameof(coercionHelper));
_optimizers = optimizers.ToArray();
@@ -109,6 +114,11 @@ private IOperation CompileOperation(
private bool IsNullBubblingEnabled(IRequestContext context, OperationDefinitionNode operationDefinition)
{
+ if (_options.AllowDisablingNullBubbling && context.ContextData.ContainsKey(DisableNullBubbling))
+ {
+ return false;
+ }
+
if (!context.Schema.ContextData.ContainsKey(EnableTrueNullability) ||
operationDefinition.Directives.Count == 0)
{
@@ -169,12 +179,14 @@ public static RequestCoreMiddleware Create()
var operationCompilerPool = core.Services.GetRequiredService>();
var optimizers1 = core.Services.GetRequiredService>();
var optimizers2 = core.SchemaServices.GetRequiredService>();
+ var options = core.SchemaServices.GetRequiredService();
var coercionHelper = core.Services.GetRequiredService();
var middleware = new OperationResolverMiddleware(
next,
operationCompilerPool,
optimizers1.Concat(optimizers2),
+ options,
coercionHelper);
return context => middleware.InvokeAsync(context);
};
-}
\ No newline at end of file
+}
diff --git a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs
index 1bb10098e6a..16895e6ff8a 100644
--- a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs
+++ b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs
@@ -55,7 +55,7 @@ public interface IReadOnlySchemaOptions
/// unreachable from the root types.
///
bool RemoveUnreachableTypes { get; }
-
+
///
/// Defines if unused type system directives shall
/// be removed from the schema.
@@ -97,7 +97,7 @@ public interface IReadOnlySchemaOptions
/// Defines if the order of important middleware components shall be validated.
///
bool ValidatePipelineOrder { get; }
-
+
///
/// Defines if the runtime types of types shall be validated.
///
diff --git a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs
index 43e3450d759..0612dbba5fb 100644
--- a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs
+++ b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs
@@ -263,7 +263,7 @@ public ISchemaBuilder AddType(INamedTypeExtension typeExtension)
_types.Add(_ => TypeReference.Create(typeExtension));
return this;
}
-
+
internal void AddTypeReference(TypeReference typeReference)
{
if (typeReference is null)
diff --git a/src/HotChocolate/Core/test/Execution.Tests/TrueNullabilityTests.cs b/src/HotChocolate/Core/test/Execution.Tests/TrueNullabilityTests.cs
index 512ce55b61d..5e4b1cebc32 100644
--- a/src/HotChocolate/Core/test/Execution.Tests/TrueNullabilityTests.cs
+++ b/src/HotChocolate/Core/test/Execution.Tests/TrueNullabilityTests.cs
@@ -15,10 +15,10 @@ public async Task Schema_Without_TrueNullability()
.AddQueryType()
.ModifyOptions(o => o.EnableTrueNullability = false)
.BuildSchemaAsync();
-
+
schema.MatchSnapshot();
}
-
+
[Fact]
public async Task Schema_With_TrueNullability()
{
@@ -28,10 +28,10 @@ public async Task Schema_With_TrueNullability()
.AddQueryType()
.ModifyOptions(o => o.EnableTrueNullability = true)
.BuildSchemaAsync();
-
+
schema.MatchSnapshot();
}
-
+
[Fact]
public async Task Error_Query_With_TrueNullability_And_NullBubbling_Enabled_By_Default()
{
@@ -51,10 +51,10 @@ public async Task Error_Query_With_TrueNullability_And_NullBubbling_Enabled_By_D
}
}
""");
-
+
response.MatchSnapshot();
}
-
+
[Fact]
public async Task Error_Query_With_TrueNullability_And_NullBubbling_Enabled()
{
@@ -74,10 +74,10 @@ query @nullBubbling {
}
}
""");
-
+
response.MatchSnapshot();
}
-
+
[Fact]
public async Task Error_Query_With_TrueNullability_And_NullBubbling_Disabled()
{
@@ -97,10 +97,10 @@ query @nullBubbling(enable: false) {
}
}
""");
-
+
response.MatchSnapshot();
}
-
+
[Fact]
public async Task Error_Query_With_TrueNullability_And_NullBubbling_Disabled_With_Variable()
{
@@ -124,24 +124,51 @@ public async Task Error_Query_With_TrueNullability_And_NullBubbling_Disabled_Wit
""")
.SetVariableValue("enable", false)
.Create());
-
+
+ response.MatchSnapshot();
+ }
+
+ [Fact]
+ public async Task Error_Query_With_NullBubbling_Disabled()
+ {
+ var request = QueryRequestBuilder.New()
+ .SetQuery("""
+ query {
+ book {
+ name
+ author {
+ name
+ }
+ }
+ }
+ """)
+ .TryAddGlobalState(WellKnownContextData.DisableNullBubbling, true)
+ .Create();
+
+ var response =
+ await new ServiceCollection()
+ .AddGraphQLServer()
+ .ModifyRequestOptions(options => options.AllowDisablingNullBubbling = true)
+ .AddQueryType()
+ .ExecuteRequestAsync(request);
+
response.MatchSnapshot();
}
-
+
public class Query
{
public Book? GetBook() => new();
}
-
+
public class Book
{
public string Name => "Some book!";
public Author Author => new();
}
-
+
public class Author
{
public string Name => throw new Exception();
- }
-}
\ No newline at end of file
+ }
+}
diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Error_Query_With_NullBubbling_Disabled.snap b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Error_Query_With_NullBubbling_Disabled.snap
new file mode 100644
index 00000000000..5f1271ea8b6
--- /dev/null
+++ b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Error_Query_With_NullBubbling_Disabled.snap
@@ -0,0 +1,26 @@
+{
+ "errors": [
+ {
+ "message": "Unexpected Execution Error",
+ "locations": [
+ {
+ "line": 5,
+ "column": 13
+ }
+ ],
+ "path": [
+ "book",
+ "author",
+ "name"
+ ]
+ }
+ ],
+ "data": {
+ "book": {
+ "name": "Some book!",
+ "author": {
+ "name": null
+ }
+ }
+ }
+}
\ No newline at end of file