Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public async Task InvokeAsync(HttpContext context)

// Verify that the request is relevant to this middleware.
&& (context.Request.Query.ContainsKey(QueryKey)
|| context.Request.Query.ContainsKey(QueryIdKey)
|| context.Request.Query.ContainsKey(IdKey)
|| context.Request.Query.ContainsKey(DocumentIdKey)
|| context.Request.Query.ContainsKey(ExtensionsKey))

// Allow ALL GET requests if we do NOT enforce preflight
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ namespace HotChocolate.AspNetCore.Parsers;
internal sealed class DefaultHttpRequestParser : IHttpRequestParser
{
internal const int MinRequestSize = 256;
internal const string QueryIdKey = "id";
internal const string DocumentIdKey = "documentId";
internal const string IdKey = "id";
private const string OperationNameKey = "operationName";
private const string OnErrorKey = "onError";
internal const string QueryKey = "query";
Expand Down Expand Up @@ -206,13 +207,19 @@ public GraphQLRequest ParseRequestFromParams(IQueryCollection parameters, bool s
query = parameters[QueryKey];
}

string? queryId = parameters[QueryIdKey];
string? documentId = parameters[IdKey];

if (string.IsNullOrWhiteSpace(documentId))
{
documentId = parameters[DocumentIdKey];
}

string? operationName = parameters[OperationNameKey];
string? onError = parameters[OnErrorKey];
JsonDocument? extensions = null;

// if we have no query or query id, we cannot execute anything.
if (string.IsNullOrWhiteSpace(query) && string.IsNullOrWhiteSpace(queryId))
// if we have no query or document ID, we cannot execute anything.
if (string.IsNullOrWhiteSpace(query) && string.IsNullOrWhiteSpace(documentId))
{
// so, if we do not find a top-level query or top-level id, we will try to parse
// the extensions and look in the extensions for Apollo's active persisted
Expand Down Expand Up @@ -247,14 +254,14 @@ public GraphQLRequest ParseRequestFromParams(IQueryCollection parameters, bool s
}
}

// if we however found a query hash, we will use it as a query id and move on
// if we however found a query hash, we will use it as a document ID and move on
// to execute the query.
queryId = hash;
documentId = hash;
}

if (!string.IsNullOrWhiteSpace(queryId))
if (!string.IsNullOrWhiteSpace(documentId))
{
EnsureValidDocumentId(queryId);
EnsureValidDocumentId(documentId);
}

try
Expand Down Expand Up @@ -285,7 +292,7 @@ public GraphQLRequest ParseRequestFromParams(IQueryCollection parameters, bool s

return new GraphQLRequest(
document,
queryId,
documentId,
documentHash,
operationName,
errorHandlingMode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,34 @@ await server.GetStoreActivePersistedQueryAsync(
new[] { resultA, resultB }.MatchSnapshot();
}

[Fact]
public async Task Get_Should_ExecutePersistedOperation_When_DocumentIdIsSet()
{
// arrange
var server = CreateStarWarsServer();
var client = server.CreateClient();

// act
using var response = await client.GetAsync(
"http://localhost:5000/graphql?documentId=60ddx_GGk4FDObSa6eK0sg",
TestContext.Current.CancellationToken);

// assert
Snapshot
.Create()
.Add(response)
.MatchInline(
"""
Headers:
Vary: Accept
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
-------------------------->
{"data":{"hero":{"name":"R2-D2"}}}
""");
}

[Fact]
public async Task Throw_Custom_GraphQL_Error()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,74 @@ void Parse()
Assert.Equal("The operation ID has an invalid format.", ex.Message);
}

[Fact]
public void ParseRequestFromParams_Should_ReadDocumentId_When_IdIsMissing()
{
// arrange
var queryParams = new QueryCollection(
new Dictionary<string, StringValues>
{
["documentId"] = "abc1213_5164-ABC-123"
});
var parser = new DefaultHttpRequestParser(
new DefaultDocumentCache(),
new Sha256DocumentHashProvider(),
256,
ParserOptions.Default);

// act
var request = parser.ParseRequestFromParams(queryParams);

// assert
Assert.Equal("abc1213_5164-ABC-123", request.DocumentId?.Value);
}

[Fact]
public void ParseRequestFromParams_Should_PreferId_When_IdAndDocumentIdAreSet()
{
// arrange
var queryParams = new QueryCollection(
new Dictionary<string, StringValues>
{
["documentId"] = "def456",
["id"] = "abc123"
});
var parser = new DefaultHttpRequestParser(
new DefaultDocumentCache(),
new Sha256DocumentHashProvider(),
256,
ParserOptions.Default);

// act
var request = parser.ParseRequestFromParams(queryParams);

// assert
Assert.Equal("abc123", request.DocumentId?.Value);
}

[Fact]
public void ParseRequestFromParams_Should_Throw_When_DocumentIdHasInvalidFormat()
{
// arrange
var queryParams = new QueryCollection(
new Dictionary<string, StringValues>
{
["documentId"] = "sha256:abc123"
});
var parser = new DefaultHttpRequestParser(
new DefaultDocumentCache(),
new Sha256DocumentHashProvider(),
256,
ParserOptions.Default);

// act
void Parse() => parser.ParseRequestFromParams(queryParams);

// assert
var ex = Assert.Throws<GraphQLRequestException>(Parse);
Assert.Equal("The operation ID has an invalid format.", ex.Message);
}

[Fact]
public async Task ParseRequestAsync_Should_AdvancePipe_When_SyntaxError()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,7 @@ Cost rejections, including the single result for a rejected variable batch, retu
Request batching is an array of independent requests in one HTTP request. Cost limits currently apply separately to each independent request in a request batch. Summing costs across an entire request batch is planned, with no target version.

Use `#!csharp RequestContext.TryGetCostAnalysisResult(out var result)` to access the compiled `CostPlan` and all estimates for the request. Cost analysis and reporting return `HC0048` when required operation or document state is missing, when a request reaches the analyzer with zero coerced variable sets (an empty variable batch executed through `IRequestExecutor`; over HTTP, the transport refuses `variables: []` before execution), or when metrics cannot be attached to the execution-result state.

## Persisted operations over GET accept `documentId`

A GET request on a GraphQL endpoint reads the operation document ID from a `documentId` query parameter as well as from `id`, the two names a POST body already accepts. A GET request that carries both uses `id`. This covers `MapGraphQL`, `MapGraphQLHttp`, and the Azure Functions integration, under every transport version. 16.6 did not execute a GET request that carried only `documentId`, so it was served Nitro or got a `404` status code.
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ In the above example, requests containing the `X-Developer` header can execute d

# Client Expectations

A client is expected to send an `id` field containing the operation document hash instead of a `query` field.
A client is expected to send an `id` or `documentId` field containing the operation document hash instead of a `query` field. Over HTTP GET, the same names are query parameters, and a request that carries both uses `id`.

**HTTP POST**

Expand All @@ -241,8 +241,14 @@ A client is expected to send an `id` field containing the operation document has
}
```

**HTTP GET**

```http
GET /graphql?id=0c95d31ca29272475bf837f944f4e513&variables={"first":5}
```

> [!NOTE]
> [Relay's persisted queries documentation](https://relay.dev/docs/guides/persisted-queries/#network-layer-changes) uses `doc_id` instead of `id`. Be sure to change it to `id`.
> [Relay's persisted queries documentation](https://relay.dev/docs/guides/persisted-queries/#network-layer-changes) uses `doc_id` instead of `id`. Be sure to change it to `id` or `documentId`.

# Next Steps

Expand Down
Loading