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