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
Expand Up @@ -125,6 +125,13 @@ internal static bool AllowErrorHandlingModeOverride(
return context.Schema.GetRequestOptions().AllowErrorHandlingModeOverride;
}

internal static bool AllowOperationPlanRequests(
this RequestContext context)
{
ArgumentNullException.ThrowIfNull(context);
return context.Schema.GetRequestOptions().AllowOperationPlanRequests;
}

internal static ISourceSchemaClientScope CreateClientScope(
this RequestContext context)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@ public bool AllowErrorHandlingModeOverride
}
}

/// <summary>
/// Gets or sets whether clients are allowed to request the operation plan
/// to be included in the GraphQL response by sending the
/// <c>Fusion-Operation-Plan</c> header.
/// <c>false</c> by default.
/// </summary>
public bool AllowOperationPlanRequests
{
get;
set
{
ExpectMutableOptions();

field = value;
}
}

/// <summary>
/// Gets or sets the persisted operation options.
/// </summary>
Expand Down Expand Up @@ -108,6 +125,7 @@ public FusionRequestOptions Clone()
ExecutionTimeout = ExecutionTimeout,
CollectOperationPlanTelemetry = CollectOperationPlanTelemetry,
AllowErrorHandlingModeOverride = AllowErrorHandlingModeOverride,
AllowOperationPlanRequests = AllowOperationPlanRequests,
PersistedOperations = PersistedOperations,
IncludeExceptionDetails = IncludeExceptionDetails
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,8 @@ internal OperationResult Complete(bool reusable = false, bool retainMemoryForDef
operationResult.Features.Set(OperationPlan);

if (OperationPlan is OperationPlan rootPlan
&& RequestContext.ContextData.ContainsKey(ExecutionContextData.IncludeOperationPlan))
&& RequestContext.ContextData.ContainsKey(ExecutionContextData.IncludeOperationPlan)
&& RequestContext.AllowOperationPlanRequests())
{
var writer = new PooledArrayWriter();
s_planFormatter.Format(writer, rootPlan, trace);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using HotChocolate.AspNetCore;
using HotChocolate.Transport.Http;
Expand Down Expand Up @@ -293,4 +295,97 @@ public async Task DefaultSecurity_Disabled_InProduction_FieldCycleDepthIsNotEnfo
Assert.Equal(JsonValueKind.Undefined, response.Errors.ValueKind);
Assert.Equal(JsonValueKind.Object, response.Data.ValueKind);
}

[Fact]
public async Task OperationPlanHeader_When_Allowed_Should_Include_OperationPlan()
{
// arrange
using var server1 = CreateSourceSchema("A", SimpleSchema);

using var gateway = await CreateCompositeSchemaAsync(
[("A", server1)],
configureGatewayBuilder: b =>
{
b.ConfigureSchemaServices((_, s) =>
{
s.RemoveAll<IHttpRequestInterceptor>();
s.AddSingleton<IHttpRequestInterceptor, DefaultHttpRequestInterceptor>();
});
b.ModifyRequestOptions(o => o.AllowOperationPlanRequests = true);
});

// act
using var response = await SendQueryAsync(gateway, includeOperationPlanHeader: true);

// assert
response.MatchSnapshot();
}

[Fact]
public async Task OperationPlanHeader_When_Not_Allowed_Should_Omit_OperationPlan()
{
// arrange
using var server1 = CreateSourceSchema("A", SimpleSchema);

using var gateway = await CreateCompositeSchemaAsync(
[("A", server1)],
configureGatewayBuilder: b =>
{
b.ConfigureSchemaServices((_, s) =>
{
s.RemoveAll<IHttpRequestInterceptor>();
s.AddSingleton<IHttpRequestInterceptor, DefaultHttpRequestInterceptor>();
});
b.ModifyRequestOptions(o => o.AllowOperationPlanRequests = false);
});

// act
using var response = await SendQueryAsync(gateway, includeOperationPlanHeader: true);

// assert
response.MatchSnapshot();
}

[Fact]
public async Task NoOperationPlanHeader_When_Allowed_Should_Omit_OperationPlan()
{
// arrange
using var server1 = CreateSourceSchema("A", SimpleSchema);

using var gateway = await CreateCompositeSchemaAsync(
[("A", server1)],
configureGatewayBuilder: b =>
{
b.ConfigureSchemaServices((_, s) =>
{
s.RemoveAll<IHttpRequestInterceptor>();
s.AddSingleton<IHttpRequestInterceptor, DefaultHttpRequestInterceptor>();
});
b.ModifyRequestOptions(o => o.AllowOperationPlanRequests = true);
});

// act
using var response = await SendQueryAsync(gateway, includeOperationPlanHeader: false);

// assert
response.MatchSnapshot();
}

private static Task<HttpResponseMessage> SendQueryAsync(Gateway gateway, bool includeOperationPlanHeader)
{
var httpClient = gateway.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost:5000/graphql");
request.Content = new StringContent(
"""{"query":"{ field }"}""",
Encoding.UTF8,
"application/json");
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/graphql-response+json"));

if (includeOperationPlanHeader)
{
request.Headers.Add("Fusion-Operation-Plan", "1");
}

return httpClient.SendAsync(request);
}
Comment thread
tobias-tengler marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ protected async Task<Gateway> CreateCompositeSchemaAsync(
{
o.CollectOperationPlanTelemetry = false;
o.AllowErrorHandlingModeOverride = true;
o.AllowOperationPlanRequests = true;
});
configureGatewayBuilder?.Invoke(gatewayBuilder);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Headers:
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
-------------------------->
{"data":{"field":"Query"}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Headers:
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
-------------------------->
{"data":{"field":"Query"},"extensions":{"fusion":{"operationPlan":{"id":"83e3f6ccc9691c16992aba024cc36790692a435fd43d0d47dfca18eeb1c43a20","operation":{"kind":"Query","document":"{\n field\n}","id":"388c5f9c43fb589b1c5d1e144121613b","hash":"388c5f9c43fb589b1c5d1e144121613b","shortHash":"388c5f9c"},"searchSpace":1,"expandedNodes":1,"nodes":[{"id":1,"type":"Operation","schema":"A","operation":{"name":"Op_388c5f9c_1","kind":"Query","document":"query Op_388c5f9c_1 {\n field\n}","hash":"d243d6aa03056cfbc2ae6272df4c2dbbafb78f04901fe6faa816dcd979e1d239","shortHash":"d243d6aa"},"resultSelectionSet":"{ field }"}]}}}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Headers:
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
-------------------------->
{"data":{"field":"Query"}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
{
"id": "68c2a64a8c04735ef1b991eeca37e895e81946d427b3c305d68fd2a19d988d51",
"operation": {
"kind": "Query",
"document": "{\n user(id: \"1\") {\n name\n }\n}",
"id": "123456789101112",
"hash": "123456789101112",
"shortHash": "12345678"
},
"searchSpace": 1,
"expandedNodes": 1,
"nodes": [
{
"id": 1,
"type": "Operation",
"schema": "a",
"operation": {
"name": "Op_123456789101112_1",
"kind": "Query",
"document": "query Op_123456789101112_1 {\n user(id: \"1\") {\n name\n }\n}",
"hash": "167b5bb497f34e9c7516ec2d4598ae6d7f0fd73a9a9a329156910e99e5db4ec3",
"shortHash": "167b5bb4"
},
"resultSelectionSet": "{ user }"
}
],
"deliveryGroups": [
{
"id": 0,
"path": "$.user",
"label": "contact"
},
{
"id": 1,
"path": "$.user",
"label": "nested",
"parentId": 0
},
{
"id": 2,
"path": "$.user",
"label": "location"
}
],
"deferredSubPlans": [
{
"deliveryGroupIds": [
0,
2
],
"parentNodeId": 1,
"operation": {
"kind": "Query",
"document": "{\n user(id: \"1\") {\n email\n id @fusion__requirement\n }\n}",
"id": "123456789101112#defer_0",
"hash": "123456789101112#defer_0",
"shortHash": "12345678"
},
"nodes": [
{
"id": 1,
"type": "Operation",
"schema": "a",
"operation": {
"name": "Op_defer_1",
"kind": "Query",
"document": "query Op_defer_1 {\n user(id: \"1\") {\n id\n }\n}",
"hash": "93841f3bbdb1c1bf3a949d7089b6713b11e6d33666aae10bdc8a885607465ca5",
"shortHash": "93841f3b"
},
"resultSelectionSet": "{ user }"
},
{
"id": 2,
"type": "Operation",
"schema": "b",
"operation": {
"name": "Op_defer_2",
"kind": "Query",
"document": "query Op_defer_2($__fusion_1_id: ID!) {\n userById(id: $__fusion_1_id) {\n email\n }\n}",
"hash": "65dbc31ddfe32ceb540eaace9ebb429fdc84b5f7758a861ec21db29c098ce8ee",
"shortHash": "65dbc31d"
},
"resultSelectionSet": "{ email }",
"source": "$.userById",
"target": "$.user",
"requirements": [
{
"name": "__fusion_1_id",
"type": "ID!",
"path": "$.user",
"selectionMap": "id"
}
],
"dependencies": [
1
]
}
]
},
{
"deliveryGroupIds": [
1
],
"parentNodeId": 2,
"operation": {
"kind": "Query",
"document": "{\n user(id: \"1\") {\n address\n id @fusion__requirement\n }\n}",
"id": "123456789101112#defer_1",
"hash": "123456789101112#defer_1",
"shortHash": "12345678"
},
"nodes": [
{
"id": 1,
"type": "Operation",
"schema": "a",
"operation": {
"name": "Op_defer_1",
"kind": "Query",
"document": "query Op_defer_1 {\n user(id: \"1\") {\n id\n }\n}",
"hash": "93841f3bbdb1c1bf3a949d7089b6713b11e6d33666aae10bdc8a885607465ca5",
"shortHash": "93841f3b"
},
"resultSelectionSet": "{ user }"
},
{
"id": 2,
"type": "Operation",
"schema": "b",
"operation": {
"name": "Op_defer_2",
"kind": "Query",
"document": "query Op_defer_2($__fusion_1_id: ID!) {\n userById(id: $__fusion_1_id) {\n address\n }\n}",
"hash": "1abf4c1bb273be32d1f61887253fa9843c5a15491e86d6962b34ecd01ad1cb01",
"shortHash": "1abf4c1b"
},
"resultSelectionSet": "{ address }",
"source": "$.userById",
"target": "$.user",
"requirements": [
{
"name": "__fusion_1_id",
"type": "ID!",
"path": "$.user",
"selectionMap": "id"
}
],
"dependencies": [
1
]
}
]
}
]
}
Loading