Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add header to disable null-bubbling #6877

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ public class DefaultHttpRequestInterceptor : IHttpRequestInterceptor
requestBuilder.TryAddGlobalState(WellKnownContextData.IncludeQueryPlan, true);
}

if (context.IsNullBubblingDisabled())
{
requestBuilder.TryAddGlobalState(WellKnownContextData.DisableNullBubbling, true);
}

return default;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ public class DefaultSocketSessionInterceptor : ISocketSessionInterceptor
requestBuilder.TryAddGlobalState(IncludeQueryPlan, true);
}

if (context.IsNullBubblingDisabled())
{
requestBuilder.TryAddGlobalState(WellKnownContextData.DisableNullBubbling, true);
}

return default;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
2 changes: 2 additions & 0 deletions src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ internal static class HttpHeaderValues

public const string IncludeQueryPlan = "1";

public const string DisableNullBubbling = "1";

public const string NoCache = "no-cache";
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,28 @@ public static HttpRequestHeaders AddGraphQLPreflight(this HttpRequestHeaders hea

headers.Add("GraphQL-Preflight", "1");
return headers;
}
}
}

/// <summary>
/// Adds the <c>GraphQL-Disable-NullBubbling</c> header to the request.
/// </summary>
/// <param name="headers">
/// The <see cref="HttpRequestHeaders"/> to add the header to.
/// </param>
/// <returns>
/// Returns the <paramref name="headers"/> for configuration chaining.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="headers"/> is <see langword="null"/>.
/// </exception>
public static HttpRequestHeaders AddGraphQLDisableNullBubbling(this HttpRequestHeaders headers)
{
if (headers == null)
{
throw new ArgumentNullException(nameof(headers));
}

headers.Add("GraphQL-Disable-NullBubbling", "1");
return headers;
}
}
16 changes: 8 additions & 8 deletions src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,6 @@ public static class WellKnownContextData
/// </summary>
public const string NodeResolver = "HotChocolate.Relay.Node.Resolver";

/// <summary>
/// The key to check if relay support is enabled.
/// </summary>
public const string IsRelaySupportEnabled = "HotChocolate.Relay.IsEnabled";

/// <summary>
/// The key to check if the global identification spec is enabled.
/// </summary>
Expand Down Expand Up @@ -274,17 +269,22 @@ public static class WellKnownContextData
/// The key to access the authorization allowed flag on the member context.
/// </summary>
public const string AllowAnonymous = "HotChocolate.Authorization.AllowAnonymous";

/// <summary>
/// The key to access the true nullability flag on the execution context.
/// </summary>
public const string EnableTrueNullability = "HotChocolate.Types.EnableTrueNullability";


/// <summary>
/// Disables null-bubbling for the current request.
/// </summary>
public const string DisableNullBubbling = "HotChocolate.Execution.DisableNullBubbling";

/// <summary>
/// The key to access the tag options object.
/// </summary>
public const string TagOptions = "HotChocolate.Types.TagOptions";

/// <summary>
/// Type key to access the internal schema options.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ namespace HotChocolate.Execution.Options;
/// </summary>
public interface IRequestExecutorOptionsAccessor
: IErrorHandlerOptionsAccessor
, IRequestTimeoutOptionsAccessor
, IComplexityAnalyzerOptionsAccessor
, IPersistedQueryOptionsAccessor;
, IRequestTimeoutOptionsAccessor
, IComplexityAnalyzerOptionsAccessor
, IPersistedQueryOptionsAccessor
{
/// <summary>
/// Determine whether null-bubbling can be disabled on a per-request basis.
/// </summary>
bool AllowDisablingNullBubbling { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,14 @@ public IError OnlyPersistedQueriesAreAllowedError
get => _onlyPersistedQueriesAreAllowedError;
set
{
_onlyPersistedQueriesAreAllowedError = value
?? throw new ArgumentNullException(
_onlyPersistedQueriesAreAllowedError = value ??
throw new ArgumentNullException(
nameof(OnlyPersistedQueriesAreAllowedError));
}
}

/// <summary>
/// Determine whether null-bubbling can be disabled on a per-request basis.
/// </summary>
public bool AllowDisablingNullBubbling { get; set; } = false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,13 +20,15 @@
{
private readonly RequestDelegate _next;
private readonly ObjectPool<OperationCompiler> _operationCompilerPool;
private readonly IRequestExecutorOptionsAccessor _options;
private readonly VariableCoercionHelper _coercionHelper;
private readonly IReadOnlyList<IOperationCompilerOptimizer>? _optimizers;

private OperationResolverMiddleware(
RequestDelegate next,
ObjectPool<OperationCompiler> operationCompilerPool,
IEnumerable<IOperationCompilerOptimizer> optimizers,
IRequestExecutorOptionsAccessor options,
VariableCoercionHelper coercionHelper)
{
if (optimizers is null)
Expand All @@ -37,6 +40,8 @@
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();
Expand Down Expand Up @@ -109,6 +114,11 @@

private bool IsNullBubblingEnabled(IRequestContext context, OperationDefinitionNode operationDefinition)
{
if (_options.AllowDisablingNullBubbling && context.ContextData.ContainsKey(DisableNullBubbling))
{

Check warning on line 118 in src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs

View check run for this annotation

Codecov / codecov/patch

src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs#L118

Added line #L118 was not covered by tests
return false;
}

Check warning on line 121 in src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs

View check run for this annotation

Codecov / codecov/patch

src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs#L120-L121

Added lines #L120 - L121 were not covered by tests
if (!context.Schema.ContextData.ContainsKey(EnableTrueNullability) ||
operationDefinition.Directives.Count == 0)
{
Expand Down Expand Up @@ -169,12 +179,14 @@
var operationCompilerPool = core.Services.GetRequiredService<ObjectPool<OperationCompiler>>();
var optimizers1 = core.Services.GetRequiredService<IEnumerable<IOperationCompilerOptimizer>>();
var optimizers2 = core.SchemaServices.GetRequiredService<IEnumerable<IOperationCompilerOptimizer>>();
var options = core.SchemaServices.GetRequiredService<IRequestExecutorOptionsAccessor>();
var coercionHelper = core.Services.GetRequiredService<VariableCoercionHelper>();
var middleware = new OperationResolverMiddleware(
next,
operationCompilerPool,
optimizers1.Concat(optimizers2),
options,
coercionHelper);
return context => middleware.InvokeAsync(context);
};
}
}
4 changes: 2 additions & 2 deletions src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public interface IReadOnlySchemaOptions
/// unreachable from the root types.
/// </summary>
bool RemoveUnreachableTypes { get; }

/// <summary>
/// Defines if unused type system directives shall
/// be removed from the schema.
Expand Down Expand Up @@ -97,7 +97,7 @@ public interface IReadOnlySchemaOptions
/// Defines if the order of important middleware components shall be validated.
/// </summary>
bool ValidatePipelineOrder { get; }

/// <summary>
/// Defines if the runtime types of types shall be validated.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/HotChocolate/Core/src/Types/SchemaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
59 changes: 43 additions & 16 deletions src/HotChocolate/Core/test/Execution.Tests/TrueNullabilityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ public async Task Schema_Without_TrueNullability()
.AddQueryType<Query>()
.ModifyOptions(o => o.EnableTrueNullability = false)
.BuildSchemaAsync();

schema.MatchSnapshot();
}

[Fact]
public async Task Schema_With_TrueNullability()
{
Expand All @@ -28,10 +28,10 @@ public async Task Schema_With_TrueNullability()
.AddQueryType<Query>()
.ModifyOptions(o => o.EnableTrueNullability = true)
.BuildSchemaAsync();

schema.MatchSnapshot();
}

[Fact]
public async Task Error_Query_With_TrueNullability_And_NullBubbling_Enabled_By_Default()
{
Expand All @@ -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()
{
Expand All @@ -74,10 +74,10 @@ public async Task Error_Query_With_TrueNullability_And_NullBubbling_Enabled()
}
}
""");

response.MatchSnapshot();
}

[Fact]
public async Task Error_Query_With_TrueNullability_And_NullBubbling_Disabled()
{
Expand All @@ -97,10 +97,10 @@ public async Task Error_Query_With_TrueNullability_And_NullBubbling_Disabled()
}
}
""");

response.MatchSnapshot();
}

[Fact]
public async Task Error_Query_With_TrueNullability_And_NullBubbling_Disabled_With_Variable()
{
Expand All @@ -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<Query>()
.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();
}
}
}
}