diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs
index d9384e95d20..72e8114c8db 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs
@@ -135,6 +135,7 @@ public static IApplicationBuilder MapGraphQL(
applicationBuilder
.Use(MiddlewareFactory.CreateCancellationMiddleware())
.Use(MiddlewareFactory.CreateWebSocketSubscriptionMiddleware(executor, serverOptions))
+ .Use(MiddlewareFactory.CreateHttpContentNegotiationMiddleware(path))
.Use(MiddlewareFactory.CreateHttpPostMiddleware(executor, serverOptions))
.Use(MiddlewareFactory.CreateHttpMultipartMiddleware(executor, serverOptions, formOptions))
.Use(MiddlewareFactory.CreateHttpQueryMiddleware(executor, serverOptions))
@@ -216,6 +217,7 @@ public static GraphQLHttpEndpointConventionBuilder MapGraphQLHttp(
requestPipeline
.Use(MiddlewareFactory.CreateCancellationMiddleware())
+ .Use(MiddlewareFactory.CreateHttpContentNegotiationMiddleware(path: null))
.Use(MiddlewareFactory.CreateHttpPostMiddleware(executor, serverOptions))
.Use(MiddlewareFactory.CreateHttpMultipartMiddleware(executor, serverOptions, formOptions))
.Use(MiddlewareFactory.CreateHttpQueryMiddleware(executor, serverOptions))
diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpRequestExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpRequestExtensions.cs
index 11f6d51f73c..ce5a82a7a1e 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpRequestExtensions.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpRequestExtensions.cs
@@ -14,6 +14,16 @@ internal static bool AcceptHeaderContainsHtml(this HttpRequest request)
&& values.Count > 0 && (values[0]?.Contains(ContentType.Html) ?? false);
}
+ internal static bool IsEndpointPath(this HttpRequest request, PathString path)
+ {
+ var isBelowPath = request.Path.StartsWithSegments(
+ path,
+ StringComparison.OrdinalIgnoreCase,
+ out var remaining);
+
+ return isBelowPath && remaining.Value is null or "" or "/";
+ }
+
internal static bool IsGetOrHeadMethod(this HttpRequest request)
{
return HttpMethods.IsGet(request.Method) || HttpMethods.IsHead(request.Method);
diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpResponseExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpResponseExtensions.cs
index bd9c014fcc2..c5c0ff89a96 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpResponseExtensions.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpResponseExtensions.cs
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Primitives;
using static System.Globalization.CultureInfo;
using static System.String;
@@ -9,6 +10,52 @@ internal static class HttpResponseExtensions
private const string ContentDispositionHeader = "Content-Disposition";
private const string ContentDispositionValue = "attachment; filename=\"{0}\"";
+ ///
+ /// Adds each comma-separated field name in to the Vary
+ /// header unless the header already lists it, compared case-insensitively, or lists *.
+ ///
+ public static void AppendVary(this IHeaderDictionary headers, string fieldNames)
+ {
+ var vary = headers.Vary;
+ var remaining = fieldNames.AsSpan();
+ var total = 0;
+ var listed = 0;
+
+ while (TryReadFieldName(ref remaining, out var fieldName))
+ {
+ total++;
+
+ if (ListsFieldName(vary, fieldName))
+ {
+ listed++;
+ }
+ }
+
+ if (listed == total)
+ {
+ return;
+ }
+
+ if (listed == 0)
+ {
+ headers.Vary = StringValues.Concat(vary, fieldNames);
+ return;
+ }
+
+ var missing = new List(total - listed);
+ remaining = fieldNames.AsSpan();
+
+ while (TryReadFieldName(ref remaining, out var fieldName))
+ {
+ if (!ListsFieldName(vary, fieldName))
+ {
+ missing.Add(fieldName.ToString());
+ }
+ }
+
+ headers.Vary = StringValues.Concat(vary, Join(", ", missing));
+ }
+
public static IHeaderDictionary SetContentDisposition(
this IHeaderDictionary headers,
string fileName)
@@ -17,4 +64,43 @@ public static IHeaderDictionary SetContentDisposition(
Format(InvariantCulture, ContentDispositionValue, fileName);
return headers;
}
+
+ private static bool ListsFieldName(StringValues values, ReadOnlySpan fieldName)
+ {
+ foreach (var value in values)
+ {
+ var remaining = value.AsSpan();
+
+ while (TryReadFieldName(ref remaining, out var listedFieldName))
+ {
+ if (listedFieldName is "*"
+ || listedFieldName.Equals(fieldName, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static bool TryReadFieldName(
+ ref ReadOnlySpan remaining,
+ out ReadOnlySpan fieldName)
+ {
+ while (!remaining.IsEmpty)
+ {
+ var comma = remaining.IndexOf(',');
+ fieldName = (comma < 0 ? remaining : remaining[..comma]).Trim();
+ remaining = comma < 0 ? [] : remaining[(comma + 1)..];
+
+ if (!fieldName.IsEmpty)
+ {
+ return true;
+ }
+ }
+
+ fieldName = default;
+ return false;
+ }
}
diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultHttpResponseFormatter.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultHttpResponseFormatter.cs
index 5ea9968f9ab..b941e9daae8 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultHttpResponseFormatter.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultHttpResponseFormatter.cs
@@ -386,7 +386,7 @@ private async ValueTask FormatInternalAsync(
if (result.ContextData.TryGetValue(ExecutionContextData.VaryHeaderValue, out var varyValue)
&& varyValue is string varyHeaderValue)
{
- response.Headers.Vary = varyHeaderValue;
+ response.Headers.AppendVary(varyHeaderValue);
}
OnWriteResponseHeaders(operationResult, format, response.Headers);
diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpContentNegotiationMiddleware.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpContentNegotiationMiddleware.cs
new file mode 100644
index 00000000000..8c9d11e4b70
--- /dev/null
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpContentNegotiationMiddleware.cs
@@ -0,0 +1,36 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.Net.Http.Headers;
+using RequestDelegate = Microsoft.AspNetCore.Http.RequestDelegate;
+
+namespace HotChocolate.AspNetCore;
+
+///
+/// Declares Accept as a selecting header, through Vary, on every request whose
+/// response the GraphQL endpoint selects by its Accept header: a GET, HEAD, POST, or QUERY
+/// on the endpoint.
+///
+///
+/// The next middleware in line.
+///
+///
+/// The path of the GraphQL endpoint, or null when every request that reaches this
+/// middleware is on the endpoint.
+///
+internal sealed class HttpContentNegotiationMiddleware(RequestDelegate next, PathString? path)
+{
+ public Task InvokeAsync(HttpContext context)
+ {
+ var request = context.Request;
+
+ if ((path is not { } endpointPath || request.IsEndpointPath(endpointPath))
+ && (HttpMethods.IsGet(request.Method)
+ || HttpMethods.IsHead(request.Method)
+ || HttpMethods.IsPost(request.Method)
+ || HttpMethods.IsQuery(request.Method)))
+ {
+ context.Response.Headers.AppendVary(HeaderNames.Accept);
+ }
+
+ return next(context);
+ }
+}
diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpUnsupportedRequestMiddleware.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpUnsupportedRequestMiddleware.cs
index ad9f4aba306..b78311d4342 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpUnsupportedRequestMiddleware.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpUnsupportedRequestMiddleware.cs
@@ -44,7 +44,7 @@ public HttpUnsupportedRequestMiddleware(
public async Task InvokeAsync(HttpContext context)
{
- if (IsGraphQLEndpoint(context.Request))
+ if (_path is not { } path || context.Request.IsEndpointPath(path))
{
var options = GetOptions(context);
@@ -92,21 +92,6 @@ public async Task InvokeAsync(HttpContext context)
context.Response.StatusCode = StatusCodes.Status404NotFound;
}
- private bool IsGraphQLEndpoint(HttpRequest request)
- {
- if (_path is not { } path)
- {
- return true;
- }
-
- var isBelowPath = request.Path.StartsWithSegments(
- path,
- StringComparison.OrdinalIgnoreCase,
- out var remaining);
-
- return isBelowPath && remaining.Value is null or "" or "/";
- }
-
// The Allow header for the options this middleware serves, built on first use.
private string GetAllow(GraphQLServerOptions options)
=> _allow ??= BuildAllow(options);
diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/MiddlewareFactory.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/MiddlewareFactory.cs
index 74af6d3606b..0170c0b127b 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/MiddlewareFactory.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/MiddlewareFactory.cs
@@ -33,6 +33,16 @@ internal static Func CreateWebSocketSubscripti
};
}
+ internal static Func CreateHttpContentNegotiationMiddleware(
+ PathString? path)
+ {
+ return next =>
+ {
+ var middleware = new HttpContentNegotiationMiddleware(next, path);
+ return context => middleware.InvokeAsync(context);
+ };
+ }
+
internal static Func CreateHttpPostMiddleware(
HttpRequestExecutorProxy executor,
GraphQLServerOptions serverOptions)
diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/PersistedOperationMiddleware.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/PersistedOperationMiddleware.cs
index f46d15e5586..b0d502d9a0f 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/PersistedOperationMiddleware.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/PersistedOperationMiddleware.cs
@@ -26,6 +26,10 @@ internal static void MapPersistedOperationMiddleware(
{
var executorProxy = HttpRequestExecutorProxy.Create(services, schemaName);
var serverOptions = services.GetRequiredService>().Get(schemaName);
+ var negotiation = MiddlewareFactory.CreateHttpContentNegotiationMiddleware(path: null);
+
+ ((IEndpointConventionBuilder)groupBuilder).Add(
+ endpoint => endpoint.RequestDelegate = negotiation(endpoint.RequestDelegate!));
groupBuilder.MapGet(
"/{operationId}",
diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Extensions/HttpResponseExtensionsTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Extensions/HttpResponseExtensionsTests.cs
new file mode 100644
index 00000000000..71c13c065bf
--- /dev/null
+++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Extensions/HttpResponseExtensionsTests.cs
@@ -0,0 +1,36 @@
+using Microsoft.AspNetCore.Http;
+
+namespace HotChocolate.AspNetCore.Extensions;
+
+public sealed class HttpResponseExtensionsTests
+{
+ [Theory]
+ [InlineData(new string[0], "Accept", new[] { "Accept" })]
+ [InlineData(new[] { "Origin" }, "Accept", new[] { "Origin", "Accept" })]
+ [InlineData(new[] { "accept" }, "Accept", new[] { "accept" })]
+ [InlineData(new[] { "Accept" }, "x-bar, x-foo", new[] { "Accept", "x-bar, x-foo" })]
+ [InlineData(new[] { "Accept" }, "accept, x-foo", new[] { "Accept", "x-foo" })]
+ [InlineData(new[] { "Origin, Accept" }, "ACCEPT", new[] { "Origin, Accept" })]
+ [InlineData(new[] { "Origin, , Accept" }, "accept", new[] { "Origin, , Accept" })]
+ [InlineData(new[] { "*" }, "Accept", new[] { "*" })]
+ [InlineData(new[] { "Origin", "*" }, "Accept, x-foo", new[] { "Origin", "*" })]
+ public void AppendVary_Should_AddOnlyNamesNotYetListed_When_NamesAreGiven(
+ string[] existing,
+ string fieldNames,
+ string[] expected)
+ {
+ // arrange
+ IHeaderDictionary headers = new HeaderDictionary();
+
+ if (existing.Length > 0)
+ {
+ headers.Vary = existing;
+ }
+
+ // act
+ headers.AppendVary(fieldNames);
+
+ // assert
+ Assert.Equal(expected, headers.Vary.ToArray());
+ }
+}
diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs
index ad04255cbc1..4ffcd80442c 100644
--- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs
+++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs
@@ -109,6 +109,7 @@ public async Task SingleResult_Success(string? acceptHeader, HttpTransportVersio
.Add(response)
.MatchInline(
@$"Headers:
+ Vary: Accept
Content-Type: {expectedContentType}
-------------------------->
Status Code: OK
@@ -140,6 +141,7 @@ public async Task SingleResult_MultipartAcceptHeader(string acceptHeader)
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: multipart/mixed; boundary="-"
-------------------------->
Status Code: OK
@@ -178,6 +180,7 @@ public async Task SingleResult_Should_WriteOneNextEvent_When_EventStreamIsAccept
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: text/event-stream; charset=utf-8
-------------------------->
Status Code: OK
@@ -235,6 +238,7 @@ public async Task Query_No_Body(string? acceptHeader, HttpTransportVersion trans
.MatchInline(
$$$"""
Headers:
+ Vary: Accept
Content-Type: {{{expectedContentType}}}
-------------------------->
Status Code: {{{expectedStatusCode}}}
@@ -276,6 +280,7 @@ public async Task ValidationError(string? acceptHeader, HttpTransportVersion tra
.Add(response)
.MatchInline(
@$"Headers:
+ Vary: Accept
Content-Type: {expectedContentType}
-------------------------->
Status Code: {expectedStatusCode}
@@ -342,6 +347,7 @@ public async Task UnsupportedAcceptHeaderValue()
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: BadRequest
@@ -371,9 +377,12 @@ public async Task UnsupportedApplicationAcceptHeaderValue()
.Add(response)
.MatchInline(
"""
+ Headers:
+ Vary: Accept
+ -------------------------->
Status Code: NotAcceptable
-------------------------->
-
+
""");
}
@@ -406,6 +415,7 @@ public async Task EventStream_Sends_KeepAlive()
.MatchInline(
"""
Headers:
+ Vary: Accept
Cache-Control: no-cache
Content-Type: text/event-stream; charset=utf-8
-------------------------->
@@ -457,6 +467,7 @@ public async Task EventStream_When_Accept_Is_All()
.MatchInline(
"""
Headers:
+ Vary: Accept
Cache-Control: no-cache
Content-Type: text/event-stream; charset=utf-8
-------------------------->
@@ -508,6 +519,7 @@ public async Task EventStream_When_Accept_Is_All_And_Subscription_Directive()
.MatchInline(
"""
Headers:
+ Vary: Accept
Cache-Control: no-cache
Content-Type: text/event-stream; charset=utf-8
-------------------------->
@@ -691,6 +703,7 @@ public async Task Post_Should_ReturnPartialSuccess_When_ResultHasDataAndErrors(
.MatchInline(
$$$"""
Headers:
+ Vary: Accept
Content-Type: {{{expectedContentType}}}
-------------------------->
Status Code: {{{expectedStatusCode}}}
@@ -732,6 +745,7 @@ public async Task Post_Should_ReturnPartialSuccess_When_NonNullViolationErasesDa
.MatchInline(
$$$"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: {{{expectedStatusCode}}}
@@ -1368,6 +1382,7 @@ public async Task Post_Should_ReturnBadRequest_When_BodyHasNoJsonToken(
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: BadRequest
@@ -1447,6 +1462,7 @@ public async Task Post_Should_ReturnInternalServerError_When_DocumentIsMissingBe
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: InternalServerError
@@ -1500,6 +1516,7 @@ public async Task Post_Should_ReturnInternalServerError_When_CachedDocumentIsMis
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: InternalServerError
@@ -1544,6 +1561,7 @@ public async Task Post_Should_ReturnUnprocessableContent_When_DocumentIdIsNotRes
.MatchInline(
$$$"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: {{{expectedStatusCode}}}
@@ -1577,6 +1595,7 @@ public async Task Post_Should_ReturnInternalServerError_When_DocumentIsMissingBe
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: InternalServerError
@@ -1610,6 +1629,7 @@ public async Task Post_Should_ReturnInternalServerError_When_OperationIsMissingB
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: InternalServerError
@@ -1646,6 +1666,7 @@ public async Task Post_Should_ReturnInternalServerError_When_VariablesAreMissing
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: InternalServerError
@@ -1686,6 +1707,7 @@ public async Task Post_Should_ReturnUnprocessableContent_When_VariableBatchIsEmp
.MatchInline(
$$$"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: {{{expectedStatusCode}}}
@@ -1745,6 +1767,7 @@ public async Task Get_Should_ReturnUnprocessableContent_When_VariableBatchIsEmpt
.MatchInline(
$$$"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: {{{expectedStatusCode}}}
@@ -1784,6 +1807,7 @@ public async Task Post_Should_ReturnUnprocessableContent_When_MultipartVariableB
.MatchInline(
$$$"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: {{{expectedStatusCode}}}
@@ -1820,6 +1844,7 @@ public async Task Post_Should_ReturnUnprocessableContent_When_PersistedVariableB
.MatchInline(
$$$"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: {{{expectedStatusCode}}}
@@ -1852,6 +1877,7 @@ public async Task Get_Should_ReturnUnprocessableContent_When_PersistedVariableBa
.MatchInline(
$$$"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: {{{expectedStatusCode}}}
@@ -1886,6 +1912,7 @@ public async Task Post_Should_ReturnBadRequest_When_RequestBatchContainsEmptyVar
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: BadRequest
@@ -1920,6 +1947,7 @@ public async Task Post_Should_ExecuteRequest_When_ContentTypeCasingVaries(string
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
@@ -2036,6 +2064,233 @@ public async Task Get_Should_ReturnMethodNotAllowed_When_GetRequestsAreDisabled(
Assert.Equal(expectedAllow, response.Content.Headers.Allow);
}
+ // A response the endpoint selects by the Accept header names Accept in Vary (RFC 9110,
+ // section 12.5.5).
+ [Theory]
+ [InlineData(Legacy)]
+ [InlineData(Draft20250508)]
+ [InlineData(Draft20260903)]
+ public async Task Get_Should_ReturnVaryAccept_When_QueryIsExecuted(
+ HttpTransportVersion transportVersion)
+ {
+ // arrange
+ var client = GetClient(transportVersion);
+ var query = Uri.EscapeDataString("{ __typename }");
+
+ // act
+ using var response = await client.GetAsync(
+ new Uri($"{s_url}?query={query}"),
+ TestContext.Current.CancellationToken);
+
+ // assert
+ Assert.Equal(OK, response.StatusCode);
+ Assert.Equal(["Accept"], response.Headers.Vary);
+ }
+
+ [Theory]
+ [InlineData(Legacy)]
+ [InlineData(Draft20250508)]
+ [InlineData(Draft20260903)]
+ public async Task Head_Should_ReturnVaryAccept_When_QueryIsExecuted(
+ HttpTransportVersion transportVersion)
+ {
+ // arrange
+ var client = GetClient(transportVersion);
+ var query = Uri.EscapeDataString("{ __typename }");
+
+ // act
+ using var request = new HttpRequestMessage(
+ HttpMethod.Head,
+ new Uri($"{s_url}?query={query}"));
+
+ using var response = await client.SendAsync(request, TestContext.Current.CancellationToken);
+
+ // assert
+ Assert.Equal(OK, response.StatusCode);
+ Assert.Equal(["Accept"], response.Headers.Vary);
+ }
+
+ [Theory]
+ [InlineData(Legacy)]
+ [InlineData(Draft20250508)]
+ [InlineData(Draft20260903)]
+ public async Task Post_Should_ReturnVaryAccept_When_QueryIsExecuted(
+ HttpTransportVersion transportVersion)
+ {
+ // arrange
+ var client = GetClient(transportVersion);
+
+ // act
+ using var response = await client.PostAsync(
+ s_url,
+ JsonContent.Create(new ClientQueryRequest { Query = "{ __typename }" }),
+ TestContext.Current.CancellationToken);
+
+ // assert
+ Assert.Equal(OK, response.StatusCode);
+ Assert.Equal(["Accept"], response.Headers.Vary);
+ }
+
+ [Theory]
+ [InlineData(Legacy)]
+ [InlineData(Draft20250508)]
+ [InlineData(Draft20260903)]
+ public async Task Query_Should_ReturnVaryAccept_When_QueryIsExecuted(
+ HttpTransportVersion transportVersion)
+ {
+ // arrange
+ var client = GetQueryClient(transportVersion);
+
+ // act
+ using var request = new HttpRequestMessage(s_queryMethod, s_url);
+ request.Content = JsonContent.Create(new ClientQueryRequest { Query = "{ __typename }" });
+
+ using var response = await client.SendAsync(request, TestContext.Current.CancellationToken);
+
+ // assert
+ Assert.Equal(OK, response.StatusCode);
+ Assert.Equal(["Accept"], response.Headers.Vary);
+ }
+
+ [Theory]
+ [InlineData(Legacy)]
+ [InlineData(Draft20250508)]
+ [InlineData(Draft20260903)]
+ public async Task Post_Should_ReturnVaryAccept_When_AcceptIsNotSupported(
+ HttpTransportVersion transportVersion)
+ {
+ // arrange
+ var client = GetClient(transportVersion);
+
+ // act
+ using var request = new HttpRequestMessage(HttpMethod.Post, s_url);
+ request.Content = JsonContent.Create(new ClientQueryRequest { Query = "{ __typename }" });
+ request.Headers.TryAddWithoutValidation(HeaderNames.Accept, "text/plain");
+
+ using var response = await client.SendAsync(request, TestContext.Current.CancellationToken);
+
+ // assert
+ Assert.Equal(NotAcceptable, response.StatusCode);
+ Assert.Equal(["Accept"], response.Headers.Vary);
+ }
+
+ [Theory]
+ [InlineData(Legacy, OK)]
+ [InlineData(Draft20250508, BadRequest)]
+ [InlineData(Draft20260903, BadRequest)]
+ public async Task Post_Should_ReturnVaryAccept_When_DocumentCannotBeParsed(
+ HttpTransportVersion transportVersion,
+ HttpStatusCode expectedStatusCode)
+ {
+ // arrange
+ var client = GetClient(transportVersion);
+
+ // act
+ using var response = await client.PostAsync(
+ s_url,
+ JsonContent.Create(new ClientQueryRequest { Query = "{" }),
+ TestContext.Current.CancellationToken);
+
+ // assert
+ Assert.Equal(expectedStatusCode, response.StatusCode);
+ Assert.Equal(["Accept"], response.Headers.Vary);
+ }
+
+ [Theory]
+ [InlineData(Legacy, NotFound)]
+ [InlineData(Draft20250508, NotFound)]
+ [InlineData(Draft20260903, MethodNotAllowed)]
+ public async Task Get_Should_ReturnVaryAccept_When_GetRequestsAreDisabled(
+ HttpTransportVersion transportVersion,
+ HttpStatusCode expectedStatusCode)
+ {
+ // arrange
+ var server = CreateStarWarsServer(
+ configureServices: s => s.AddGraphQLServer().AddHttpResponseFormatter(
+ new HttpResponseFormatterOptions
+ {
+ HttpTransportVersion = transportVersion
+ }),
+ configureConventions: b => b.WithOptions(o =>
+ {
+ o.EnableGetRequests = false;
+ o.Tool.Enable = false;
+ }));
+ var client = server.CreateClient();
+ var query = Uri.EscapeDataString("{ __typename }");
+
+ // act
+ using var response = await client.GetAsync(
+ new Uri($"{s_url}?query={query}"),
+ TestContext.Current.CancellationToken);
+
+ // assert
+ Assert.Equal(expectedStatusCode, response.StatusCode);
+ Assert.Equal(["Accept"], response.Headers.Vary);
+ }
+
+ [Theory]
+ [InlineData(Legacy, "OPTIONS")]
+ [InlineData(Draft20250508, "OPTIONS")]
+ [InlineData(Draft20260903, "OPTIONS")]
+ [InlineData(Legacy, "PUT")]
+ [InlineData(Draft20250508, "PUT")]
+ [InlineData(Draft20260903, "PUT")]
+ public async Task Request_Should_NotReturnVary_When_MethodDoesNotSelectResponseByAccept(
+ HttpTransportVersion transportVersion,
+ string method)
+ {
+ // arrange
+ var client = GetClient(transportVersion);
+
+ // act
+ using var request = new HttpRequestMessage(new HttpMethod(method), s_url);
+
+ using var response = await client.SendAsync(request, TestContext.Current.CancellationToken);
+
+ // assert
+ Assert.Empty(response.Headers.Vary);
+ }
+
+ [Fact]
+ public async Task Upgrade_Should_NotReturnVary_When_WebSocketIsAccepted()
+ {
+ // arrange
+ using var server = CreateStarWarsServer();
+ var client = server.CreateWebSocketClient();
+ client.SubProtocols.Add("graphql-transport-ws");
+ HttpContext? serverContext = null;
+ client.ConfigureRequest = request => serverContext = request.HttpContext;
+
+ // act
+ using var webSocket = await client.ConnectAsync(
+ new Uri("ws://localhost:5000/graphql"),
+ TestContext.Current.CancellationToken);
+
+ // assert
+ Assert.Equal("graphql-transport-ws", webSocket.SubProtocol);
+ Assert.NotNull(serverContext);
+ Assert.Empty(serverContext.Response.Headers.Vary.ToArray());
+ }
+
+ [Fact]
+ public async Task Post_Should_ReturnVaryAccept_When_EndpointIsMappedWithMapGraphQLHttp()
+ {
+ // arrange
+ var server = CreateServer(endpoints => endpoints.MapGraphQLHttp());
+ var client = server.CreateClient();
+
+ // act
+ using var response = await client.PostAsync(
+ s_url,
+ JsonContent.Create(new ClientQueryRequest { Query = "{ __typename }" }),
+ TestContext.Current.CancellationToken);
+
+ // assert
+ Assert.Equal(OK, response.StatusCode);
+ Assert.Equal(["Accept"], response.Headers.Vary);
+ }
+
// When QUERY requests are enabled the endpoint lists the method in Allow and advertises the
// body media type it accepts through Accept-Query (RFC 10008, section 3).
[Fact]
@@ -2430,6 +2685,7 @@ public async Task Query_Should_ReturnUnprocessableContent_When_VariableBatchIsEm
.MatchInline(
$$$"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: {{{expectedStatusCode}}}
@@ -2487,6 +2743,7 @@ public async Task Query_Should_ReturnBadRequest_When_BodyHasNoJsonToken(
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: BadRequest
@@ -2651,9 +2908,12 @@ public async Task SingleResult_Should_ReturnNotAcceptable_When_EveryMediaTypeIsR
.Add(response)
.MatchInline(
"""
+ Headers:
+ Vary: Accept
+ -------------------------->
Status Code: NotAcceptable
-------------------------->
-
+
""");
}
diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpContentNegotiationMiddlewareTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpContentNegotiationMiddlewareTests.cs
new file mode 100644
index 00000000000..c54078ca58c
--- /dev/null
+++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpContentNegotiationMiddlewareTests.cs
@@ -0,0 +1,95 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Primitives;
+
+namespace HotChocolate.AspNetCore;
+
+public sealed class HttpContentNegotiationMiddlewareTests
+{
+ [Theory]
+ [InlineData("/graphql", "GET", "/graphql")]
+ [InlineData("/graphql", "HEAD", "/graphql")]
+ [InlineData("/graphql", "POST", "/graphql")]
+ [InlineData("/graphql", "QUERY", "/graphql")]
+ [InlineData("/graphql", "GET", "/graphql/")]
+ [InlineData("/graphql", "GET", "/GraphQL")]
+ [InlineData("", "GET", "/")]
+ [InlineData(null, "GET", "/anything")]
+ [InlineData(null, "POST", "/")]
+ public async Task InvokeAsync_Should_AddAcceptToVary_When_EndpointSelectsResponseByAccept(
+ string? endpointPath,
+ string method,
+ string requestPath)
+ {
+ // arrange
+ var context = CreateContext(method, requestPath);
+ var nextRan = false;
+ var middleware = CreateMiddleware(endpointPath, () => nextRan = true);
+
+ // act
+ await middleware.InvokeAsync(context);
+
+ // assert
+ Assert.Equal(new StringValues("Accept"), context.Response.Headers.Vary);
+ Assert.True(nextRan);
+ }
+
+ [Theory]
+ [InlineData("/graphql", "OPTIONS", "/graphql")]
+ [InlineData("/graphql", "PUT", "/graphql")]
+ [InlineData("/graphql", "DELETE", "/graphql")]
+ [InlineData("/graphql", "GET", "/graphql/nitro-config.json")]
+ [InlineData("/graphql", "GET", "/graphql-x")]
+ [InlineData("/graphql", "GET", "/other")]
+ [InlineData(null, "OPTIONS", "/anything")]
+ public async Task InvokeAsync_Should_LeaveVaryEmpty_When_EndpointDoesNotSelectResponseByAccept(
+ string? endpointPath,
+ string method,
+ string requestPath)
+ {
+ // arrange
+ var context = CreateContext(method, requestPath);
+ var nextRan = false;
+ var middleware = CreateMiddleware(endpointPath, () => nextRan = true);
+
+ // act
+ await middleware.InvokeAsync(context);
+
+ // assert
+ Assert.Equal(StringValues.Empty, context.Response.Headers.Vary);
+ Assert.True(nextRan);
+ }
+
+ [Fact]
+ public async Task InvokeAsync_Should_KeepExistingVaryValues_When_VaryIsAlreadySet()
+ {
+ // arrange
+ var context = CreateContext("GET", "/graphql");
+ context.Response.Headers.Vary = "Origin";
+ var middleware = CreateMiddleware("/graphql", () => { });
+
+ // act
+ await middleware.InvokeAsync(context);
+
+ // assert
+ Assert.Equal(new StringValues(["Origin", "Accept"]), context.Response.Headers.Vary);
+ }
+
+ private static DefaultHttpContext CreateContext(string method, string requestPath)
+ {
+ var context = new DefaultHttpContext();
+ context.Request.Method = method;
+ context.Request.Path = requestPath;
+ return context;
+ }
+
+ private static HttpContentNegotiationMiddleware CreateMiddleware(
+ string? endpointPath,
+ Action onNext)
+ => new(
+ _ =>
+ {
+ onNext();
+ return Task.CompletedTask;
+ },
+ endpointPath is null ? (PathString?)null : new PathString(endpointPath));
+}
diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSchemaMiddlewareTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSchemaMiddlewareTests.cs
index a64a37d6345..21694a8d330 100644
--- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSchemaMiddlewareTests.cs
+++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSchemaMiddlewareTests.cs
@@ -54,6 +54,7 @@ public async Task Download_GraphQL_Schema(string path)
// assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
response.Headers.Remove("ETag");
+ response.Headers.Remove("Vary");
response.Content.Headers.ContentLength = null;
response.MatchMarkdownSnapshot();
@@ -83,6 +84,7 @@ public async Task Download_GraphQL_Schema_Slicing_Args_Enabled(string path)
// assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
response.Headers.Remove("ETag");
+ response.Headers.Remove("Vary");
response.Content.Headers.ContentLength = null;
response.MatchMarkdownSnapshot();
diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs
index b8e0645fd5f..f6f51a54d3a 100644
--- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs
+++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs
@@ -220,6 +220,7 @@ await server.PostHttpAsync(
result.MatchInlineSnapshot(
"""
Headers:
+ Vary: Accept
abc: def
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
@@ -807,6 +808,7 @@ public async Task Strip_Null_Values_Variant_1()
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
@@ -842,6 +844,7 @@ public async Task Strip_Null_Values_Variant_2()
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
@@ -882,6 +885,7 @@ public async Task Strip_Null_Elements()
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpQueryMiddlewareTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpQueryMiddlewareTests.cs
index cea76f4cccf..24a4cea5bbb 100644
--- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpQueryMiddlewareTests.cs
+++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpQueryMiddlewareTests.cs
@@ -34,6 +34,7 @@ public async Task Query_Should_ReturnResult_When_QueryRequestsAreEnabled()
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
@@ -67,6 +68,7 @@ public async Task Query_Should_ReturnResult_When_VariablesAreSent()
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
@@ -179,6 +181,7 @@ public async Task Query_Should_ReturnBadRequest_When_BodyIsArray()
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: BadRequest
@@ -214,6 +217,7 @@ public async Task Query_Should_ReturnBadRequest_When_BodyIsEmptyArray()
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: BadRequest
@@ -251,6 +255,7 @@ public async Task Query_Should_ReturnBadRequest_When_BatchOperationsIsSet()
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: BadRequest
@@ -292,6 +297,7 @@ public async Task Query_Should_ReturnBadRequest_When_VariablesIsArray()
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: BadRequest
@@ -345,6 +351,7 @@ public async Task Query_Should_RunRequestInterceptor_When_RequestIsCreated()
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: BadRequest
diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/PersistedOperationMiddlewareTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/PersistedOperationMiddlewareTests.cs
index 087a4837020..bd06cef50bc 100644
--- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/PersistedOperationMiddlewareTests.cs
+++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/PersistedOperationMiddlewareTests.cs
@@ -266,6 +266,7 @@ public async Task Query_Should_ExecutePersistedOperation_When_QueryRequestsAreEn
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
@@ -302,6 +303,7 @@ public async Task Query_Should_ApplyVariables_When_BodyCarriesVariables()
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
@@ -335,6 +337,7 @@ public async Task Query_Should_ReturnUnprocessableContent_When_PersistedMutation
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: UnprocessableEntity
@@ -371,6 +374,7 @@ public async Task Query_Should_ReturnBadRequest_When_VariablesIsArray()
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: BadRequest
@@ -400,4 +404,35 @@ public async Task Query_Should_ReturnNotFound_When_QueryRequestsAreDisabled()
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
Assert.Empty(response.Content.Headers.Allow);
}
+
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("POST")]
+ [InlineData("QUERY")]
+ public async Task Request_Should_ReturnVaryAccept_When_PersistedOperationIsExecuted(
+ string method)
+ {
+ // arrange
+ var server = CreateStarWarsServer(
+ configureServices: s => s
+ .AddGraphQLServer()
+ .ModifyServerOptions(o => o.EnableQueryRequests = true));
+ var client = server.CreateClient();
+
+ // act
+ using var request = new HttpRequestMessage(
+ new HttpMethod(method),
+ "http://localhost:5000/graphql/persisted/60ddx_GGk4FDObSa6eK0sg/GetHeroName");
+
+ if (method is not "GET")
+ {
+ request.Content = new StringContent("{ }", Encoding.UTF8, "application/json");
+ }
+
+ using var response = await client.SendAsync(request, TestContext.Current.CancellationToken);
+
+ // assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal(["Accept"], response.Headers.Vary);
+ }
}
diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/IntrospectionTests.Introspection_Request_When_Development_Success.md b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/IntrospectionTests.Introspection_Request_When_Development_Success.md
index 1efb9a8fda7..b0452d883a9 100644
--- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/IntrospectionTests.Introspection_Request_When_Development_Success.md
+++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/IntrospectionTests.Introspection_Request_When_Development_Success.md
@@ -2,6 +2,7 @@
```text
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/IntrospectionTests.Introspection_Request_When_NOT_Development_Fail.md b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/IntrospectionTests.Introspection_Request_When_NOT_Development_Fail.md
index 450702931bd..20b61308890 100644
--- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/IntrospectionTests.Introspection_Request_When_NOT_Development_Fail.md
+++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/IntrospectionTests.Introspection_Request_When_NOT_Development_Fail.md
@@ -2,6 +2,7 @@
```text
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: BadRequest
diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/IntrospectionTests.Introspection_Request_With_Rule_Removed_Fail.md b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/IntrospectionTests.Introspection_Request_With_Rule_Removed_Fail.md
index 93f8d7b358f..df59657e722 100644
--- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/IntrospectionTests.Introspection_Request_With_Rule_Removed_Fail.md
+++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/IntrospectionTests.Introspection_Request_With_Rule_Removed_Fail.md
@@ -2,6 +2,7 @@
```text
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/AzureHeaderDictionary.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/AzureHeaderDictionary.cs
index bcb3737108e..ffb8ce3737d 100644
--- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/AzureHeaderDictionary.cs
+++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/AzureHeaderDictionary.cs
@@ -75,6 +75,7 @@ public StringValues this[string key]
set
{
_response.Headers[key] = value;
+ _responseData.Headers.Remove(key);
_responseData.Headers.Add(key, (IEnumerable)value);
}
}
@@ -85,6 +86,7 @@ public long? ContentLength
set
{
_response.Headers.ContentLength = value;
+ _responseData.Headers.Remove(HeaderNames.ContentLength);
_responseData.Headers.Add(
HeaderNames.ContentLength,
(string?)_response.Headers[HeaderNames.ContentLength]);
diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/HotChocolate.AzureFunctions.IsolatedProcess.csproj b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/HotChocolate.AzureFunctions.IsolatedProcess.csproj
index 9e073c63f46..43d73457780 100644
--- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/HotChocolate.AzureFunctions.IsolatedProcess.csproj
+++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/HotChocolate.AzureFunctions.IsolatedProcess.csproj
@@ -7,6 +7,10 @@
This package contains the GraphQL AzureFunctions Isolated Process integration for Hot Chocolate.
+
+
+
+
diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs
index b564f75e1e6..1334c698606 100644
--- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs
+++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs
@@ -96,6 +96,7 @@ private static IServiceCollection AddAzureFunctionsGraphQLRequestExecutor(
var pipeline = new PipelineBuilder()
.Use(MiddlewareFactory.CreateCancellationMiddleware())
.Use(MiddlewareFactory.CreateWebSocketSubscriptionMiddleware(executor, serverOptions))
+ .Use(MiddlewareFactory.CreateHttpContentNegotiationMiddleware(path))
.Use(MiddlewareFactory.CreateHttpPostMiddleware(executor, serverOptions))
.Use(MiddlewareFactory.CreateHttpMultipartMiddleware(executor, serverOptions, formOptions))
.Use(MiddlewareFactory.CreateHttpQueryMiddleware(executor, serverOptions))
diff --git a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/AzureHeaderDictionaryTests.cs b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/AzureHeaderDictionaryTests.cs
new file mode 100644
index 00000000000..85943f62001
--- /dev/null
+++ b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/AzureHeaderDictionaryTests.cs
@@ -0,0 +1,45 @@
+using HotChocolate.AzureFunctions.IsolatedProcess.Tests.Helpers;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Primitives;
+
+namespace HotChocolate.AzureFunctions.IsolatedProcess.Tests;
+
+public sealed class AzureHeaderDictionaryTests
+{
+ [Fact]
+ public void Indexer_Should_ReplaceResponseDataValues_When_HeaderIsSetAgain()
+ {
+ // arrange
+ using var functionContext = new MockFunctionContext(
+ new ServiceCollection().BuildServiceProvider());
+ using var responseData = new MockHttpResponseData(functionContext);
+ var headers = new AzureHeaderDictionary(new DefaultHttpContext().Response, responseData);
+ headers["Vary"] = "Accept";
+
+ // act
+ headers["Vary"] = new StringValues(["Accept", "x-foo"]);
+
+ // assert
+ Assert.Equal(["Accept", "x-foo"], responseData.Headers.GetValues("Vary"));
+ }
+
+ [Fact]
+ public void ContentLength_Should_ReplaceResponseDataValue_When_ContentLengthIsSetAgain()
+ {
+ // arrange
+ using var functionContext = new MockFunctionContext(
+ new ServiceCollection().BuildServiceProvider());
+ using var responseData = new MockHttpResponseData(functionContext);
+ var headers = new AzureHeaderDictionary(new DefaultHttpContext().Response, responseData)
+ {
+ ContentLength = 10
+ };
+
+ // act
+ headers.ContentLength = 20;
+
+ // assert
+ Assert.Equal(["20"], responseData.Headers.GetValues("Content-Length"));
+ }
+}
diff --git a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessEndToEndTests.cs b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessEndToEndTests.cs
index 47c346bd83a..926ec29f998 100644
--- a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessEndToEndTests.cs
+++ b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessEndToEndTests.cs
@@ -1,5 +1,7 @@
+using System.Net;
using System.Text;
using HotChocolate.AzureFunctions.IsolatedProcess.Tests.Helpers;
+using HotChocolate.AzureFunctions.Tests.Helpers;
using HotChocolate.Types;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
@@ -93,6 +95,55 @@ public async Task AzFuncIsolatedProcess_FunctionsContextItemsTestAsync()
Assert.Equal("Darth Vader", json.data.person.ToString());
}
+ [Fact]
+ public async Task Post_Should_ReturnVaryAccept_When_QueryIsExecuted()
+ {
+ // arrange
+ var host = new MockIsolatedProcessHostBuilder()
+ .AddGraphQLFunction(
+ b => b.AddQueryType(
+ d => d.Name("Query").Field("person").Resolve("Luke Skywalker")))
+ .Build();
+ var requestExecutor = host.Services.GetRequiredService();
+ var request = TestHttpRequestDataHelper.NewGraphQLHttpRequestData(
+ host.Services,
+ "{ person }");
+
+ // act
+ var response = await requestExecutor.ExecuteAsync(request);
+
+ // assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.True(response.Headers.TryGetValues("Vary", out var vary));
+ Assert.Equal(["Accept"], vary);
+ }
+
+ [Fact]
+ public async Task Get_Should_ReturnVaryAccept_When_QueryIsExecuted()
+ {
+ // arrange
+ var host = new MockIsolatedProcessHostBuilder()
+ .AddGraphQLFunction(
+ b => b.AddQueryType(
+ d => d.Name("Query").Field("person").Resolve("Luke Skywalker")))
+ .Build();
+ var requestExecutor = host.Services.GetRequiredService();
+ var request = new MockHttpRequestData(
+ new MockFunctionContext(host.Services),
+ HttpMethods.Get,
+ new Uri(
+ TestHttpContextHelper.DefaultAzFuncGraphQLUri,
+ "?query=%7B%20person%20%7D"));
+
+ // act
+ var response = await requestExecutor.ExecuteAsync(request);
+
+ // assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.True(response.Headers.TryGetValues("Vary", out var vary));
+ Assert.Equal(["Accept"], vary);
+ }
+
[Fact]
public async Task AzFuncIsolatedProcess_NitroTestAsync()
{
diff --git a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs
index 71e154d23d9..3139a620366 100644
--- a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs
+++ b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs
@@ -1,5 +1,6 @@
using HotChocolate.AzureFunctions.Tests.Helpers;
using HotChocolate.Types;
+using Microsoft.AspNetCore.Http;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq;
@@ -39,6 +40,52 @@ public async Task AzFuncInProcess_EndToEndTestAsync()
Assert.Equal("Luke Skywalker", json.data.person.ToString());
}
+ [Fact]
+ public async Task Post_Should_ReturnVaryAccept_When_QueryIsExecuted()
+ {
+ // arrange
+ var hostBuilder = new MockInProcessFunctionsHostBuilder();
+ hostBuilder
+ .AddGraphQLFunction()
+ .AddQueryType(d => d.Name("Query").Field("person").Resolve("Luke Skywalker"));
+ var requestExecutor = hostBuilder
+ .BuildServiceProvider()
+ .GetRequiredService();
+ var httpContext = TestHttpContextHelper.NewGraphQLHttpContext("{ person }");
+
+ // act
+ await requestExecutor.ExecuteAsync(httpContext.Request);
+
+ // assert
+ Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);
+ Assert.Equal("Accept", httpContext.Response.Headers.Vary.ToString());
+ }
+
+ [Fact]
+ public async Task Get_Should_ReturnVaryAccept_When_QueryIsExecuted()
+ {
+ // arrange
+ var hostBuilder = new MockInProcessFunctionsHostBuilder();
+ hostBuilder
+ .AddGraphQLFunction()
+ .AddQueryType(d => d.Name("Query").Field("person").Resolve("Luke Skywalker"));
+ var requestExecutor = hostBuilder
+ .BuildServiceProvider()
+ .GetRequiredService();
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Request.Path = TestHttpContextHelper.DefaultAzFuncGraphQLUri.AbsolutePath;
+ httpContext.Request.QueryString = new QueryString("?query=%7B%20person%20%7D");
+ httpContext.Response.Body = new MemoryStream();
+
+ // act
+ await requestExecutor.ExecuteAsync(httpContext.Request);
+
+ // assert
+ Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);
+ Assert.Equal("Accept", httpContext.Response.Headers.Vary.ToString());
+ }
+
[Fact]
public async Task AzFuncInProcess_NitroTestAsync()
{
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/HttpCachingTests.cs b/src/HotChocolate/Caching/test/Caching.Tests/HttpCachingTests.cs
index 497a6cf5bce..7064ac768a3 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/HttpCachingTests.cs
+++ b/src/HotChocolate/Caching/test/Caching.Tests/HttpCachingTests.cs
@@ -413,6 +413,31 @@ public async Task SharedMaxAgeAndVary_Multiple_Should_Cache_And_Combine()
result.MatchSnapshot();
}
+
+ [Fact]
+ public async Task SharedMaxAgeAndVary_Should_ListAcceptOnce_When_VaryNamesAccept()
+ {
+ // arrange
+ var server = CreateServer(services =>
+ {
+ services.AddGraphQLServer()
+ .UseQueryCache()
+ .AddCacheControl()
+ .ModifyCacheControlOptions(o => o.ApplyDefaults = false)
+ .AddQueryType(d =>
+ d.Name("Query")
+ .Field("field")
+ .Resolve("")
+ .CacheControl(sharedMaxAge: 2000, vary: ["Accept", "X-foo"]));
+ });
+ var client = server.CreateClient();
+
+ // act
+ var result = await client.PostQueryAsync("{ field }");
+
+ // assert
+ Assert.Equal(["Accept", "x-foo"], result.Headers.Vary);
+ }
}
public class GraphQLResult
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Default_Max_Age_Should_Apply_And_Cache.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Default_Max_Age_Should_Apply_And_Cache.snap
index 1537c24a358..0b34f742f46 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Default_Max_Age_Should_Apply_And_Cache.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Default_Max_Age_Should_Apply_And_Cache.snap
@@ -1,5 +1,11 @@
{
"Headers": [
+ {
+ "Key": "Vary",
+ "Value": [
+ "Accept"
+ ]
+ },
{
"Key": "Cache-Control",
"Value": [
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Default_Scope_Should_Apply_And_Cache.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Default_Scope_Should_Apply_And_Cache.snap
index 2cef0413fbf..2a9e8e184fa 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Default_Scope_Should_Apply_And_Cache.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Default_Scope_Should_Apply_And_Cache.snap
@@ -1,5 +1,11 @@
{
"Headers": [
+ {
+ "Key": "Vary",
+ "Value": [
+ "Accept"
+ ]
+ },
{
"Key": "Cache-Control",
"Value": [
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.JustScope_Should_Not_Cache.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.JustScope_Should_Not_Cache.snap
index ade977b218d..eb2d9c636c8 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.JustScope_Should_Not_Cache.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.JustScope_Should_Not_Cache.snap
@@ -1,5 +1,12 @@
{
- "Headers": [],
+ "Headers": [
+ {
+ "Key": "Vary",
+ "Value": [
+ "Accept"
+ ]
+ }
+ ],
"ContentHeaders": [
{
"Key": "Content-Type",
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Just_Defaults_Should_Cache.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Just_Defaults_Should_Cache.snap
index 8c2402b5538..048ab5f459c 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Just_Defaults_Should_Cache.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Just_Defaults_Should_Cache.snap
@@ -1,5 +1,11 @@
{
"Headers": [
+ {
+ "Key": "Vary",
+ "Value": [
+ "Accept"
+ ]
+ },
{
"Key": "Cache-Control",
"Value": [
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAgeAndScope_Should_Cache.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAgeAndScope_Should_Cache.snap
index 30e93764014..7b7b742de07 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAgeAndScope_Should_Cache.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAgeAndScope_Should_Cache.snap
@@ -1,5 +1,11 @@
{
"Headers": [
+ {
+ "Key": "Vary",
+ "Value": [
+ "Accept"
+ ]
+ },
{
"Key": "Cache-Control",
"Value": [
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_Multiple_Combine_Public_Private_Caches_Private.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_Multiple_Combine_Public_Private_Caches_Private.snap
index 1b0c1084995..4136bbce255 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_Multiple_Combine_Public_Private_Caches_Private.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_Multiple_Combine_Public_Private_Caches_Private.snap
@@ -1,5 +1,11 @@
{
"Headers": [
+ {
+ "Key": "Vary",
+ "Value": [
+ "Accept"
+ ]
+ },
{
"Key": "Cache-Control",
"Value": [
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_Multiple_Should_Cache_Shortest_Time.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_Multiple_Should_Cache_Shortest_Time.snap
index ed213d3da11..bc4b74acfa3 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_Multiple_Should_Cache_Shortest_Time.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_Multiple_Should_Cache_Shortest_Time.snap
@@ -1,5 +1,11 @@
{
"Headers": [
+ {
+ "Key": "Vary",
+ "Value": [
+ "Accept"
+ ]
+ },
{
"Key": "Cache-Control",
"Value": [
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_NonZero_Should_Cache.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_NonZero_Should_Cache.snap
index 663a3206cc0..71a282a92db 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_NonZero_Should_Cache.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_NonZero_Should_Cache.snap
@@ -1,5 +1,11 @@
{
"Headers": [
+ {
+ "Key": "Vary",
+ "Value": [
+ "Accept"
+ ]
+ },
{
"Key": "Cache-Control",
"Value": [
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_SharedMaxAge_Combine_Produces_Resolved_Cache.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_SharedMaxAge_Combine_Produces_Resolved_Cache.snap
index af5c26c2539..88bb68dda14 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_SharedMaxAge_Combine_Produces_Resolved_Cache.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_SharedMaxAge_Combine_Produces_Resolved_Cache.snap
@@ -1,5 +1,11 @@
{
"Headers": [
+ {
+ "Key": "Vary",
+ "Value": [
+ "Accept"
+ ]
+ },
{
"Key": "Cache-Control",
"Value": [
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_Zero_Should_Cache.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_Zero_Should_Cache.snap
index 8c2402b5538..048ab5f459c 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_Zero_Should_Cache.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_Zero_Should_Cache.snap
@@ -1,5 +1,11 @@
{
"Headers": [
+ {
+ "Key": "Vary",
+ "Value": [
+ "Accept"
+ ]
+ },
{
"Key": "Cache-Control",
"Value": [
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.No_Applied_Defaults_Should_Not_Cache.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.No_Applied_Defaults_Should_Not_Cache.snap
index ade977b218d..eb2d9c636c8 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.No_Applied_Defaults_Should_Not_Cache.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.No_Applied_Defaults_Should_Not_Cache.snap
@@ -1,5 +1,12 @@
{
- "Headers": [],
+ "Headers": [
+ {
+ "Key": "Vary",
+ "Value": [
+ "Accept"
+ ]
+ }
+ ],
"ContentHeaders": [
{
"Key": "Content-Type",
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.QueryError_Should_Not_Cache.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.QueryError_Should_Not_Cache.snap
index 802b210230f..d14fb50e890 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.QueryError_Should_Not_Cache.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.QueryError_Should_Not_Cache.snap
@@ -1,5 +1,12 @@
{
- "Headers": [],
+ "Headers": [
+ {
+ "Key": "Vary",
+ "Value": [
+ "Accept"
+ ]
+ }
+ ],
"ContentHeaders": [
{
"Key": "Content-Type",
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndScope_Should_Cache.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndScope_Should_Cache.snap
index 7f2b807a438..02b5434d769 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndScope_Should_Cache.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndScope_Should_Cache.snap
@@ -1,5 +1,11 @@
{
"Headers": [
+ {
+ "Key": "Vary",
+ "Value": [
+ "Accept"
+ ]
+ },
{
"Key": "Cache-Control",
"Value": [
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndVary_Multiple_Should_Cache_And_Combine.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndVary_Multiple_Should_Cache_And_Combine.snap
index ff8488a4571..04b69b42d41 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndVary_Multiple_Should_Cache_And_Combine.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndVary_Multiple_Should_Cache_And_Combine.snap
@@ -1,18 +1,19 @@
{
"Headers": [
- {
- "Key": "Cache-Control",
- "Value": [
- "s-maxage=1000"
- ]
- },
{
"Key": "Vary",
"Value": [
+ "Accept",
"x-bar",
"x-far",
"x-foo"
]
+ },
+ {
+ "Key": "Cache-Control",
+ "Value": [
+ "s-maxage=1000"
+ ]
}
],
"ContentHeaders": [
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndVary_Should_Cache.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndVary_Should_Cache.snap
index 7cfa21bfbf6..246ed1648c5 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndVary_Should_Cache.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndVary_Should_Cache.snap
@@ -1,16 +1,17 @@
{
"Headers": [
{
- "Key": "Cache-Control",
+ "Key": "Vary",
"Value": [
- "s-maxage=2000"
+ "Accept",
+ "x-bar",
+ "x-foo"
]
},
{
- "Key": "Vary",
+ "Key": "Cache-Control",
"Value": [
- "x-bar",
- "x-foo"
+ "s-maxage=2000"
]
}
],
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAge_MaxAge_Combine_Produces_Resolved_Cache.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAge_MaxAge_Combine_Produces_Resolved_Cache.snap
index af5c26c2539..88bb68dda14 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAge_MaxAge_Combine_Produces_Resolved_Cache.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAge_MaxAge_Combine_Produces_Resolved_Cache.snap
@@ -1,5 +1,11 @@
{
"Headers": [
+ {
+ "Key": "Vary",
+ "Value": [
+ "Accept"
+ ]
+ },
{
"Key": "Cache-Control",
"Value": [
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAge_Multiple_Combine_Public_Private_Caches_Private.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAge_Multiple_Combine_Public_Private_Caches_Private.snap
index dae75be78e8..5b24282be35 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAge_Multiple_Combine_Public_Private_Caches_Private.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAge_Multiple_Combine_Public_Private_Caches_Private.snap
@@ -1,5 +1,11 @@
{
"Headers": [
+ {
+ "Key": "Vary",
+ "Value": [
+ "Accept"
+ ]
+ },
{
"Key": "Cache-Control",
"Value": [
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/VariableBatchingTests.cs b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/VariableBatchingTests.cs
index c015af6b256..64d605dae97 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/VariableBatchingTests.cs
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/VariableBatchingTests.cs
@@ -169,6 +169,7 @@ public async Task Execute_Should_ReturnRequestError_When_VariableBatchIsEmpty(
.MatchInline(
$$$"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: {{{expectedStatusCode}}}
@@ -223,6 +224,7 @@ public async Task Execute_Should_ReturnInternalServerError_When_CoercedVariables
.MatchInline(
"""
Headers:
+ Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: InternalServerError
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/CostReportingTests.RejectedRequest_Should_ReturnHttp200_When_AcceptIsLegacyJson.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/CostReportingTests.RejectedRequest_Should_ReturnHttp200_When_AcceptIsLegacyJson.snap
index 2f11b1e9bc4..6a4e191e721 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/CostReportingTests.RejectedRequest_Should_ReturnHttp200_When_AcceptIsLegacyJson.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/CostReportingTests.RejectedRequest_Should_ReturnHttp200_When_AcceptIsLegacyJson.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/json; charset=utf-8
-------------------------->
Status Code: OK
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/CostReportingTests.RejectedRequest_Should_ReturnHttp400_When_AcceptIsGraphQLResponseJson.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/CostReportingTests.RejectedRequest_Should_ReturnHttp400_When_AcceptIsGraphQLResponseJson.snap
index ef92e45e90d..cb2aa357d93 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/CostReportingTests.RejectedRequest_Should_ReturnHttp400_When_AcceptIsGraphQLResponseJson.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/CostReportingTests.RejectedRequest_Should_ReturnHttp400_When_AcceptIsGraphQLResponseJson.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: BadRequest
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_False_With_OperationPlanHeader_Should_Omit_OperationPlan.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_False_With_OperationPlanHeader_Should_Omit_OperationPlan.snap
index 3b59182c2b5..ee5de0c0ecb 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_False_With_OperationPlanHeader_Should_Omit_OperationPlan.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_False_With_OperationPlanHeader_Should_Omit_OperationPlan.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_False_With_PerRequestOverride_And_OperationPlanHeader_Should_Include_OperationPlan.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_False_With_PerRequestOverride_And_OperationPlanHeader_Should_Include_OperationPlan.snap
index 9e9a4528906..9d71b2a795f 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_False_With_PerRequestOverride_And_OperationPlanHeader_Should_Include_OperationPlan.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_False_With_PerRequestOverride_And_OperationPlanHeader_Should_Include_OperationPlan.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_False_With_PerRequestOverride_Without_OperationPlanHeader_Should_Omit_OperationPlan.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_False_With_PerRequestOverride_Without_OperationPlanHeader_Should_Omit_OperationPlan.snap
index 3b59182c2b5..ee5de0c0ecb 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_False_With_PerRequestOverride_Without_OperationPlanHeader_Should_Omit_OperationPlan.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_False_With_PerRequestOverride_Without_OperationPlanHeader_Should_Omit_OperationPlan.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_True_With_OperationPlanHeader_Should_Include_OperationPlan.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_True_With_OperationPlanHeader_Should_Include_OperationPlan.snap
index 9e9a4528906..9d71b2a795f 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_True_With_OperationPlanHeader_Should_Include_OperationPlan.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_True_With_OperationPlanHeader_Should_Include_OperationPlan.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_True_Without_OperationPlanHeader_Should_Omit_OperationPlan.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_True_Without_OperationPlanHeader_Should_Omit_OperationPlan.snap
index 3b59182c2b5..ee5de0c0ecb 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_True_Without_OperationPlanHeader_Should_Omit_OperationPlan.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DefaultSecurityTests.AllowOperationPlanRequests_True_Without_OperationPlanHeader_Should_Omit_OperationPlan.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.ApolloStyle_MD5Hash_NotFound.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.ApolloStyle_MD5Hash_NotFound.snap
index a62e20834e6..a188806b7f3 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.ApolloStyle_MD5Hash_NotFound.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.ApolloStyle_MD5Hash_NotFound.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: BadRequest
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.ApolloStyle_MD5Hash_Success.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.ApolloStyle_MD5Hash_Success.snap
index 14447b4c81a..3682a5bfe2b 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.ApolloStyle_MD5Hash_Success.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.ApolloStyle_MD5Hash_Success.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.ApolloStyle_Sha1Hash_Success.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.ApolloStyle_Sha1Hash_Success.snap
index 14447b4c81a..3682a5bfe2b 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.ApolloStyle_Sha1Hash_Success.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.ApolloStyle_Sha1Hash_Success.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.ApolloStyle_Sha256Hash_Success.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.ApolloStyle_Sha256Hash_Success.snap
index 14447b4c81a..3682a5bfe2b 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.ApolloStyle_Sha256Hash_Success.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.ApolloStyle_Sha256Hash_Success.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_MD5Hash_NotFound.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_MD5Hash_NotFound.snap
index a62e20834e6..a188806b7f3 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_MD5Hash_NotFound.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_MD5Hash_NotFound.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: BadRequest
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_MD5Hash_Success.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_MD5Hash_Success.snap
index 14447b4c81a..3682a5bfe2b 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_MD5Hash_Success.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_MD5Hash_Success.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_Sha1Hash_Success.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_Sha1Hash_Success.snap
index 14447b4c81a..3682a5bfe2b 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_Sha1Hash_Success.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_Sha1Hash_Success.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_Sha256Hash_Query_Empty_String_Success.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_Sha256Hash_Query_Empty_String_Success.snap
index 14447b4c81a..3682a5bfe2b 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_Sha256Hash_Query_Empty_String_Success.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_Sha256Hash_Query_Empty_String_Success.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_Sha256Hash_Success.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_Sha256Hash_Success.snap
index 14447b4c81a..3682a5bfe2b 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_Sha256Hash_Success.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.HotChocolateStyle_Sha256Hash_Success.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Allowed_When_Persisted.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Allowed_When_Persisted.snap
index 14447b4c81a..3682a5bfe2b 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Allowed_When_Persisted.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Allowed_When_Persisted.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_By_Default_Works.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_By_Default_Works.snap
index 14447b4c81a..3682a5bfe2b 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_By_Default_Works.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_By_Default_Works.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Not_Allowed.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Not_Allowed.snap
index cab2a6bc158..f0f4ca69e06 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Not_Allowed.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Not_Allowed.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: BadRequest
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Not_Allowed_Custom_Error.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Not_Allowed_Custom_Error.snap
index 9e07a2f23b7..e1ac8e1f716 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Not_Allowed_Custom_Error.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Not_Allowed_Custom_Error.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: BadRequest
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Not_Allowed_Even_When_Persisted.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Not_Allowed_Even_When_Persisted.snap
index cab2a6bc158..f0f4ca69e06 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Not_Allowed_Even_When_Persisted.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Not_Allowed_Even_When_Persisted.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: BadRequest
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Not_Allowed_Override_Per_Request.snap b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Not_Allowed_Override_Per_Request.snap
index 14447b4c81a..3682a5bfe2b 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Not_Allowed_Override_Per_Request.snap
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/PersistedOperationTests.Standard_Query_Not_Allowed_Override_Per_Request.snap
@@ -1,4 +1,5 @@
Headers:
+Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
diff --git a/website/content/docs/fusion/cache-control.md b/website/content/docs/fusion/cache-control.md
index 013654c53eb..95acb34fcef 100644
--- a/website/content/docs/fusion/cache-control.md
+++ b/website/content/docs/fusion/cache-control.md
@@ -93,7 +93,7 @@ type Query {
Fusion computes one effective response policy by traversing the planned operation tree. It starts at the selected root fields, reads `@cacheControl` metadata on each field, falls back to the field return type when values are missing, and continues recursively through child selections, including interfaces and unions.
-All collected constraints are merged into one final policy. The merge is conservative: `max-age` and `s-maxage` take the lowest value, scope resolves to the strictest value (`private` over `public`), and `vary` values are merged, normalized, and deduplicated.
+All collected constraints are merged into one final policy. The merge is conservative: `max-age` and `s-maxage` take the lowest value, scope resolves to the strictest value (`private` over `public`), and `vary` values are merged, normalized, and deduplicated. Every GraphQL response already lists `Accept` in `Vary`, and the merged `vary` names are added to it. Some CDNs, Akamai by default, do not cache a response whose `Vary` lists anything other than `Accept-Encoding`. Configure the CDN to cache it anyway, or remove `Accept` from `Vary` in a [custom formatter](../hotchocolate/server/http-transport.md#adding-selecting-headers-to-vary).
Fusion computes these cache constraints for query operations. Mutation requests, Subscription request, introspection requests, and operations with no cache constraints do not get cache-control headers.
diff --git a/website/content/docs/hotchocolate/migrating/migrate-from-16-6-to-16-7.md b/website/content/docs/hotchocolate/migrating/migrate-from-16-6-to-16-7.md
index 35b536c2e6e..bc2c98af525 100644
--- a/website/content/docs/hotchocolate/migrating/migrate-from-16-6-to-16-7.md
+++ b/website/content/docs/hotchocolate/migrating/migrate-from-16-6-to-16-7.md
@@ -265,6 +265,14 @@ A `406` has no `Content-Type` and no body when the header also rejects the forma
A client that relies on the 16.6 format keeps it by rating that format highest in its `Accept` header.
+## Responses carry `Vary: Accept`
+
+Every response to a GET, HEAD, POST, or QUERY request on a GraphQL endpoint, other than a WebSocket upgrade, lists `Accept` in its `Vary` header, whatever its status code, under every transport version. This covers `MapGraphQL`, `MapGraphQLHttp`, `MapGraphQLPersistedOperations`, and the Azure Functions integration. 16.6 did not list `Accept` in `Vary`.
+
+The `vary` names of `@cacheControl` are added beside `Accept`. 16.6 wrote them in place of any `Vary` value that application middleware had set earlier in the request.
+
+A CDN or reverse proxy that honors `Vary` stores one cached response per distinct `Accept` value. Some CDNs do not cache a response whose `Vary` lists anything other than `Accept-Encoding`. Akamai behaves this way by default, so cached GET responses stop being cached at the edge after the update. Configure the CDN to cache them anyway, for example with Akamai's [Remove Vary Header](https://techdocs.akamai.com/property-mgr/docs/rm-vary-header) behavior, or remove `Accept` from `Vary` in a [custom formatter](../server/http-transport.md#adding-selecting-headers-to-vary).
+
# Noteworthy changes
## New cost options
diff --git a/website/content/docs/hotchocolate/server/cache-control.md b/website/content/docs/hotchocolate/server/cache-control.md
index 54e29e99f7b..224c7a4d01e 100644
--- a/website/content/docs/hotchocolate/server/cache-control.md
+++ b/website/content/docs/hotchocolate/server/cache-control.md
@@ -111,7 +111,7 @@ public static class Query
Hot Chocolate computes one effective response policy by traversing the selected query fields. It reads `@cacheControl` metadata on each field, falls back to the field return type when values are missing, and continues recursively through child selections.
-All collected constraints are merged into one final policy. The merge is conservative: `max-age` and `s-maxage` take the lowest value, scope resolves to the strictest value (`private` over `public`), and `vary` values are merged, normalized, and deduplicated.
+All collected constraints are merged into one final policy. The merge is conservative: `max-age` and `s-maxage` take the lowest value, scope resolves to the strictest value (`private` over `public`), and `vary` values are merged, normalized, and deduplicated. Every GraphQL response already lists `Accept` in `Vary`, and the merged `vary` names are added to it. Some CDNs, Akamai by default, do not cache a response whose `Vary` lists anything other than `Accept-Encoding`. Configure the CDN to cache it anyway, or remove `Accept` from `Vary` in a [custom formatter](./http-transport.md#adding-selecting-headers-to-vary).
Hot Chocolate computes cache constraints only for query operations. Introspection requests and operations for which no selected field contributes `maxAge` or `sharedMaxAge` do not produce a cache policy. `UseQueryCache()` writes the final headers only when the executed result has no GraphQL errors and the request has not opted out of cache-control header generation.
diff --git a/website/content/docs/hotchocolate/server/http-transport.md b/website/content/docs/hotchocolate/server/http-transport.md
index 05a69b6f302..aa40f120cdf 100644
--- a/website/content/docs/hotchocolate/server/http-transport.md
+++ b/website/content/docs/hotchocolate/server/http-transport.md
@@ -22,6 +22,8 @@ A single result is written as `multipart/mixed` or `text/event-stream` only when
When the client sends `Accept: application/json`, the response `Content-Type` is `application/json`. Under `Draft20250508`, the default transport version, every request the server reads is then answered with a `200` status code, including one that fails validation or asks for an operation kind the request method does not allow; only a request it cannot read, such as a body that is not valid JSON or a request that is not a well-formed GraphQL over HTTP request, and a batch it does not accept have a `400` status code. Under `Draft20260903`, the response takes the same status code as `application/graphql-response+json`, and only a `2xx` response carries `Content-Type: application/json`.
+Every response to a GET, HEAD, POST, or QUERY request on the GraphQL endpoint, other than a WebSocket upgrade, carries `Vary: Accept`, whatever its status code. A cache that honors `Vary` stores one response per distinct `Accept` value.
+
# Types of Requests
GraphQL requests over HTTP can be performed via the POST, GET, or QUERY HTTP verb.
@@ -200,6 +202,26 @@ public class CustomHttpResponseFormatter : DefaultHttpResponseFormatter
}
```
+## Adding Selecting Headers to Vary
+
+The endpoint adds `Accept` to the `Vary` header before the formatter runs. A custom formatter that selects a response by further request headers adds them to `Vary` without replacing it:
+
+```csharp
+public class CustomHttpResponseFormatter : DefaultHttpResponseFormatter
+{
+ protected override void OnWriteResponseHeaders(
+ OperationResult result,
+ FormatInfo format,
+ IHeaderDictionary headers)
+ {
+ headers.Append(HeaderNames.Vary, HeaderNames.AcceptLanguage);
+ base.OnWriteResponseHeaders(result, format, headers);
+ }
+}
+```
+
+A formatter can also remove `Accept` from `Vary` in the same method, keeping the other names in the header, for a cache that does not store responses whose `Vary` lists `Accept`.
+
# JSON Serialization
You can alter some JSON serialization settings when configuring the `HttpResponseFormatter`.