Skip to content

Commit 1225348

Browse files
committed
fix over http
1 parent 51769b4 commit 1225348

4 files changed

Lines changed: 68 additions & 8 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
# 6.0.0
2+
3+
EntityGraphQL 6.0 is the culmination of the 6.0.0-beta1 through 6.0.0-beta9 releases. The beta sections below carry the full, detailed lists of changes, fixes and migration notes - and see the [upgrade guide](https://entitygraphql.github.io/6.0/upgrade-6-0) for a consolidated migration path from 5.x.
4+
5+
## 6.0 Highlights
6+
7+
- **GraphQL spec conformance** - partial results per the spec (failing top-level fields no longer fail the whole operation), [September 2025 GraphQL spec](https://spec.graphql.org/September2025/) support (`@oneOf`, argument/input-field deprecation, document descriptions, stricter validation) and `MapGraphQL()` follows the [GraphQL over HTTP spec](https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md) by default.
8+
- **A real async model** - `ResolveAsync<TService>()` fields, end-to-end `CancellationToken` support (into resolvers, mutations, subscriptions and the in-flight EF query), async database materialization for root list fields, and concurrency control per field, per service and per query (`MaxQueryConcurrency` defaults to 100).
9+
- **Security hardening** - authorization fails closed, introspection respects authorization, a parser recursion backstop against deeply-nested document DoS, opt-in query guards (`MaxQueryDepth`, `MaxFieldSelections`, `MaxFieldAliases`, `MaxQueryComplexity`) and per-field rate limiting.
10+
- **New schema-building features** - `UseAggregate()` for SQL-translated aggregates over collections, `AddFieldsFrom<T>()`/`AddQueryFieldsFrom<T>()` to group field definitions into classes, filter improvements (GraphQL variables, service fields, `selectMany`, filtering by paged child fields) and async subscription setup for external brokers.
11+
- **Performance** - paging answers `hasNextPage` with a cheap `EXISTS` instead of `COUNT(*)`, async result processing is compiled and targeted (~2x faster on async-heavy results), opt-in compiled-delegate caching, and WebSocket subscription delivery is ordered and non-blocking.
12+
- **Modern targets** - `net8.0`, `net9.0` and `net10.0` with a dependency-light core package.
13+
14+
## Breaking Changes from Beta 9
15+
16+
- `MapGraphQL()` HTTP status codes now fully follow the [GraphQL over HTTP spec](https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md) for the `application/graphql-response+json` media type (the default response type): a response with no `data` entry (a request error - e.g. document parse/validation failure) returns `400` instead of `200`, and a response with both `data` and `errors` (partial success) returns `294` per the spec. Clean results remain `200`. Responses negotiated as legacy `application/json` keep returning `200` for every well-formed request. Clients following the spec read the response body regardless of status code for `application/graphql-response+json`.
17+
118
# 6.0.0-beta9
219

320
The final beta planned before 6.0.0.

‎docs/docs/integration.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ Being GraphQL there are many tools that integrate well with EntityGraphQL.
88

99
EntityGraphQL supports GraphQL introspection queries so tools like GraphiQL and Relay can work against your schema.
1010

11+
:::tip GraphQL over HTTP
12+
`MapGraphQL()` follows the [GraphQL over HTTP spec](https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md), the behavior EntityGraphQL adopted in v6. GraphQL execution errors are returned in the GraphQL response body rather than by changing the route shape or adding a special endpoint mode. HTTP status codes follow the spec per negotiated media type: with `application/graphql-response+json` (the default) a request error (no `data` in the response) returns `400`, a partial success (`data` plus `errors`) returns `294` and a clean result `200`; with the legacy `application/json` every well-formed request returns `200` and clients must read the body. Either way, always read `errors` from the response body.
13+
:::
14+
1115
## Introspection
1216

1317
Introspection is **enabled by default** and is required by most GraphQL client tools (GraphiQL, Insomnia, Relay dev tools, Apollo Studio, code generators, etc.). Disabling it is not recommended unless your schema must be kept confidential.

‎src/EntityGraphQL.AspNet/Extensions/EntityGraphQLEndpointRouteExtensions.cs‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,25 @@ public static IEndpointRouteBuilder MapGraphQL<TQueryType>(
115115

116116
context.Response.ContentType = requestedType ?? $"{APP_GQL_TYPE_START}; charset=utf-8";
117117

118-
// Per GraphQL over HTTP spec: GraphQL errors should return 200 with error details
119-
context.Response.StatusCode = StatusCodes.Status200OK;
118+
// Status codes per the GraphQL over HTTP spec. For application/graphql-response+json the
119+
// status reflects the GraphQL outcome (clients should still read the body - the code aids
120+
// intermediaries/observability): no data entry means a request error (4xx - the spec
121+
// recommends 400/422 per case; we use 400 as the errors don't distinguish parse from
122+
// validation), data plus errors is a partial success (294 per the spec), otherwise 200.
123+
// For legacy application/json the spec requires 200 for every well-formed request
124+
if (context.Response.ContentType.StartsWith(APP_GQL_TYPE_START, StringComparison.InvariantCulture))
125+
{
126+
if (!gqlResult.HasDataKey)
127+
context.Response.StatusCode = StatusCodes.Status400BadRequest;
128+
else if (gqlResult.HasErrors())
129+
context.Response.StatusCode = 294; // partial success
130+
else
131+
context.Response.StatusCode = StatusCodes.Status200OK;
132+
}
133+
else
134+
{
135+
context.Response.StatusCode = StatusCodes.Status200OK;
136+
}
120137

121138
var serializer = context.RequestServices.GetRequiredService<IGraphQLResponseSerializer>();
122139
await serializer.SerializeAsync(context.Response.Body, gqlResult);

‎src/tests/EntityGraphQL.AspNet.Tests/EntityGraphQLEndpointRouteExtensionsTests.cs‎

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,17 @@ public async Task GraphQL_Endpoint_Returns_Correct_ResponseType_Error()
8181
}
8282

8383
[Fact]
84-
public async Task GraphQL_Endpoint_Returns_Ok_With_Error_For_Invalid_Query()
84+
public async Task GraphQL_Endpoint_Returns_400_With_Error_For_Invalid_Query()
8585
{
8686
var graphqlRequest = new { query = "query { nonExistentField }" };
8787
var requestBody = new StringContent(System.Text.Json.JsonSerializer.Serialize(graphqlRequest), Encoding.UTF8, "application/json");
8888
var request = new HttpRequestMessage(HttpMethod.Post, "/graphql") { Content = requestBody };
8989
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/graphql-response+json"));
9090
var response = await client.SendAsync(request);
9191

92-
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
92+
// per the GraphQL over HTTP spec, with application/graphql-response+json a response with no
93+
// data entry (a request error) uses a 4xx status code
94+
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
9395
Assert.True(
9496
response.Content.Headers.ContentType?.MediaType == "application/graphql-response+json" || response.Content.Headers.ContentType?.MediaType == "application/json",
9597
"Content-Type is not application/graphql-response+json or application/json."
@@ -150,15 +152,16 @@ public async Task GraphQL_Endpoint_400_On_Invalid_Json_Fields()
150152
}
151153

152154
[Fact]
153-
public async Task GraphQL_Endpoint_200_On_Valid_Request_Invalid_GraphQL()
155+
public async Task GraphQL_Endpoint_400_On_Valid_Request_Invalid_GraphQL()
154156
{
155157
var graphqlRequest = new { query = "{" }; // invalid graphql
156158
var requestBody = new StringContent(System.Text.Json.JsonSerializer.Serialize(graphqlRequest), Encoding.UTF8, "application/json");
157159
var request = new HttpRequestMessage(HttpMethod.Post, "/graphql") { Content = requestBody };
158160
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/graphql-response+json"));
159161
var response = await client.SendAsync(request);
160162

161-
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
163+
// document parse failure -> no data entry -> 400 for application/graphql-response+json
164+
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
162165

163166
var responseString = await response.Content.ReadAsStringAsync();
164167
var json = JsonNode.Parse(responseString);
@@ -170,15 +173,16 @@ public async Task GraphQL_Endpoint_200_On_Valid_Request_Invalid_GraphQL()
170173
}
171174

172175
[Fact]
173-
public async Task GraphQL_Endpoint_200_No_Data_Mutation_Error()
176+
public async Task GraphQL_Endpoint_294_No_Data_Mutation_Error()
174177
{
175178
var graphqlRequest = new { query = "mutation { mutationFail }" };
176179
var requestBody = new StringContent(System.Text.Json.JsonSerializer.Serialize(graphqlRequest), Encoding.UTF8, "application/json");
177180
var request = new HttpRequestMessage(HttpMethod.Post, "/graphql") { Content = requestBody };
178181
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/graphql-response+json"));
179182
var response = await client.SendAsync(request);
180183

181-
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
184+
// data entry present (null) with errors -> partial success -> 294 per the GraphQL over HTTP spec
185+
Assert.Equal(294, (int)response.StatusCode);
182186

183187
var responseString = await response.Content.ReadAsStringAsync();
184188
var json = JsonNode.Parse(responseString);
@@ -299,6 +303,24 @@ public async Task GraphQL_Endpoint_FollowSpec_Supports_Not_First()
299303
await CheckResponseIsValid(response);
300304
}
301305

306+
[Fact]
307+
public async Task GraphQL_Endpoint_LegacyJson_Returns_200_For_Invalid_Query()
308+
{
309+
var graphqlRequest = new { query = "query { nonExistentField }" };
310+
var requestBody = new StringContent(System.Text.Json.JsonSerializer.Serialize(graphqlRequest), Encoding.UTF8, "application/json");
311+
var request = new HttpRequestMessage(HttpMethod.Post, "/graphql") { Content = requestBody };
312+
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
313+
var response = await client.SendAsync(request);
314+
315+
// per the GraphQL over HTTP spec, the legacy application/json media type uses 200 for every
316+
// response to a well-formed GraphQL-over-HTTP request - errors are in the body
317+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
318+
var responseString = await response.Content.ReadAsStringAsync();
319+
var json = JsonNode.Parse(responseString);
320+
Assert.NotNull(json);
321+
Assert.NotNull(json["errors"]);
322+
}
323+
302324
private static async Task CheckResponseIsValid(HttpResponseMessage response)
303325
{
304326
var responseString = await response.Content.ReadAsStringAsync();

0 commit comments

Comments
 (0)