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
24 changes: 18 additions & 6 deletions src/HotChocolate/Caching/test/Caching.Tests/HttpCachingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -365,12 +365,15 @@ public async Task SharedMaxAgeAndScope_Should_Cache()
result.MatchSnapshot();
}

[Fact]
public async Task SharedMaxAgeAndVary_Should_Cache()
[Theory]
[InlineData("POST")]
[InlineData("QUERY")]
public async Task SharedMaxAgeAndVary_Should_Cache(string method)
{
var server = CreateServer(services =>
{
services.AddGraphQLServer()
.ModifyServerOptions(o => o.EnableQueryRequests = true)
.UseQueryCache()
.AddCacheControl()
.ModifyCacheControlOptions(o => o.ApplyDefaults = false)
Expand All @@ -382,7 +385,7 @@ public async Task SharedMaxAgeAndVary_Should_Cache()
});

var client = server.CreateClient();
var result = await client.PostQueryAsync("{ field }");
var result = await client.SendQueryAsync(new HttpMethod(method), "{ field }");

result.MatchSnapshot();
}
Expand Down Expand Up @@ -451,13 +454,22 @@ public class GraphQLResult

internal static class TestServerExtensions
{
public static async Task<GraphQLResult> PostQueryAsync(this HttpClient client, string query)
public static Task<GraphQLResult> PostQueryAsync(this HttpClient client, string query)
=> client.SendQueryAsync(HttpMethod.Post, query);

public static async Task<GraphQLResult> SendQueryAsync(
this HttpClient client,
HttpMethod method,
string query)
{
var payload = $"{{ \"query\": \"{query}\" }}";

var content = new StringContent(payload, Encoding.UTF8, "application/json");
using var request = new HttpRequestMessage(method, "/graphql")
{
Content = new StringContent(payload, Encoding.UTF8, "application/json")
};

var response = await client.PostAsync("/graphql", content);
var response = await client.SendAsync(request);

var result = new GraphQLResult
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,10 @@ public async Task Query_With_Private_Scope_Should_Return_Private_Header()
Assert.Equal(TimeSpan.FromSeconds(3600), response.Headers.CacheControl.MaxAge);
}

[Fact]
public async Task Query_With_SharedMaxAge_Should_Return_SMaxAge_Header()
[Theory]
[InlineData("POST")]
[InlineData("QUERY")]
public async Task Query_With_SharedMaxAge_Should_Return_SMaxAge_Header(string method)
{
// arrange
using var server = CreateSourceSchema(
Expand All @@ -99,12 +101,13 @@ public async Task Query_With_SharedMaxAge_Should_Return_SMaxAge_Header()
using var gateway = await CreateCompositeSchemaAsync(
[("a", server)],
configureGatewayBuilder: b => b
.ModifyServerOptions(o => o.EnableQueryRequests = true)
.AddCacheControl()
.UseQueryCache());

// act
var httpClient = gateway.CreateClient();
var response = await httpClient.PostGraphQLAsync("{ sharedData }");
var response = await httpClient.SendGraphQLAsync(new HttpMethod(method), "{ sharedData }");

// assert
Assert.NotNull(response.Headers.CacheControl);
Expand Down Expand Up @@ -295,11 +298,20 @@ public class Mutation

internal static class TestHttpClientExtensions
{
public static async Task<HttpResponseMessage> PostGraphQLAsync(
this HttpClient client, string query)
public static Task<HttpResponseMessage> PostGraphQLAsync(this HttpClient client, string query)
=> client.SendGraphQLAsync(HttpMethod.Post, query);

public static async Task<HttpResponseMessage> SendGraphQLAsync(
this HttpClient client,
HttpMethod method,
string query)
{
var payload = $$"""{"query":"{{query}}"}""";
var content = new StringContent(payload, Encoding.UTF8, "application/json");
return await client.PostAsync("/graphql", content);
using var request = new HttpRequestMessage(method, "/graphql")
{
Content = new StringContent(payload, Encoding.UTF8, "application/json")
};

return await client.SendAsync(request);
}
}
25 changes: 24 additions & 1 deletion website/content/docs/fusion/cache-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Good cache rules help you:

To make caching work safely and predictably, you need two things:

1. A deterministic GET route so caches get a stable cache key.
1. A deterministic GET route, or a QUERY request, so caches get a stable cache key.
2. Correct Cache-Control (and Vary) headers so caches know where and how long a response may be reused.

# Why GraphQL Needs Extra Care
Expand Down Expand Up @@ -74,6 +74,29 @@ var app = builder.Build();
app.MapGraphQLPersistedOperations();
```

# QUERY Requests

Query operations can also be sent over HTTP QUERY, which carries the same JSON body as a POST request. QUERY requests are disabled by default. Enable them on the gateway with `EnableQueryRequests`:

```csharp
builder
.AddGraphQLGateway()
.ModifyServerOptions(o => o.EnableQueryRequests = true);
```

```http
QUERY /graphql
Content-Type: application/json
Accept: application/graphql-response+json

{
"query": "query GetProducts($first: Int!) { products(first: $first) { nodes { name } } }",
"variables": { "first": 5 }
}
```

[RFC 10008](https://www.rfc-editor.org/rfc/rfc10008.html#section-2.7) makes the response to a QUERY request cacheable, and a cache that stores it includes the request body in the cache key. A QUERY response carries the same `Cache-Control` and `Vary` headers as a GET response. Support for caching QUERY responses varies between CDNs and proxies, so GET routes and persisted-operation GET routes are the portable choice. See [QUERY Requests](../hotchocolate/server/http-transport.md#query-requests) for the full request rules.

# `@cacheControl` in GraphQL

A deterministic route alone is not enough. The gateway also needs policy metadata to decide whether a response is public or private, and how long it may be reused.
Expand Down
25 changes: 24 additions & 1 deletion website/content/docs/hotchocolate/server/cache-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Good cache rules help you:

To make caching work safely and predictably, you need two things:

1. A deterministic GET route so caches get a stable cache key.
1. A deterministic GET route, or a QUERY request, so caches get a stable cache key.
2. Correct Cache-Control (and Vary) headers so caches know where and how long a response may be reused.

# Why GraphQL Needs Extra Care
Expand Down Expand Up @@ -74,6 +74,29 @@ var app = builder.Build();
app.MapGraphQLPersistedOperations();
```

# QUERY Requests

Query operations can also be sent over HTTP QUERY, which carries the same JSON body as a POST request. QUERY requests are disabled by default. Enable them with `EnableQueryRequests`:

```csharp
builder
.AddGraphQL()
.ModifyServerOptions(o => o.EnableQueryRequests = true);
```

```http
QUERY /graphql
Content-Type: application/json
Accept: application/graphql-response+json

{
"query": "query GetProducts($first: Int!) { products(first: $first) { nodes { name } } }",
"variables": { "first": 5 }
}
```

[RFC 10008](https://www.rfc-editor.org/rfc/rfc10008.html#section-2.7) makes the response to a QUERY request cacheable, and a cache that stores it includes the request body in the cache key. A QUERY response carries the same `Cache-Control` and `Vary` headers as a GET response. Support for caching QUERY responses varies between CDNs and proxies, so GET routes and persisted-operation GET routes are the portable choice. See [QUERY Requests](./http-transport.md#query-requests) for the full request rules.

# `@cacheControl` in GraphQL

A deterministic route alone is not enough. The gateway also needs policy metadata to decide whether a response is public or private, and how long it may be reused.
Expand Down
Loading