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 @@ -44,6 +44,7 @@ public class DefaultHttpResponseFormatter : IHttpResponseFormatter
private readonly FormatInfo[] _streamPreferred;
private readonly IncrementalDeliveryFormat _incrementalDeliveryDefaultFormat;
private readonly bool _jsonFollowsGraphQLResponseRules;
private readonly bool _reportsContentTooLarge;
private readonly bool _reportsPartialSuccess;
private readonly bool _reportsUnprocessableRequest;

Expand Down Expand Up @@ -140,13 +141,15 @@ public DefaultHttpResponseFormatter(

// From the 2026-09-03 revision on, a client that accepts application/json is answered as
// if it had asked for application/graphql-response+json, and only a 2xx response is
// written with application/json as its Content-Type. The same revision answers a result
// that carries errors beside its data with 294, a request the server read but cannot
// execute with 422 rather than 400, and a request whose method or Content-Type the
// endpoint does not support with 405 or 415 rather than 404.
// written with application/json as its Content-Type. The same revision answers a request
// body over the maximum request size with 413 rather than 400, a result that carries
// errors beside its data with 294, a request the server read but cannot execute with 422
// rather than 400, and a request whose method or Content-Type the endpoint does not
// support with 405 or 415 rather than 404.
var usesRevision20260903 = TransportVersion is not
(HttpTransportVersion.Legacy or HttpTransportVersion.Draft20250508);
_jsonFollowsGraphQLResponseRules = usesRevision20260903;
_reportsContentTooLarge = usesRevision20260903;
_reportsPartialSuccess = usesRevision20260903;
_reportsUnprocessableRequest = usesRevision20260903;
ReportsUnsupportedMethodOrMediaType = usesRevision20260903;
Expand Down Expand Up @@ -622,6 +625,15 @@ protected virtual HttpStatusCode OnDetermineStatusCode(
return HttpStatusCode.UnprocessableContent;
}

// From the 2026-09-03 revision on, a request body over the maximum request size
// is answered 413.
if (_reportsContentTooLarge
&& proposedStatusCode is HttpStatusCode.BadRequest
&& result.ContextData.ContainsKey(HttpResultContextData.RequestTooLarge))
{
return HttpStatusCode.RequestEntityTooLarge;
}

return proposedStatusCode.Value;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ private async ValueTask<ReadResult> ReadFullBodyAsync(
if (result.Buffer.Length > _maxRequestSize)
{
requestBody.AdvanceTo(result.Buffer.End);
throw new GraphQLRequestException("Request size exceeds maximum allowed size.");
throw DefaultHttpRequestParser_MaxRequestSizeExceeded();
}

if (result.IsCompleted || result.IsCanceled)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<value>Either the parameter query or the parameter id has to be set.</value>
</data>
<data name="ThrowHelper_DefaultHttpRequestParser_MaxRequestSizeExceeded" xml:space="preserve">
<value>Max GraphQL request size reached.</value>
<value>Request size exceeds maximum allowed size.</value>
</data>
<data name="ThrowHelper_DataStartMessageHandler_RequestTypeNotSupported" xml:space="preserve">
<value>The response type is not supported.</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ internal static class HttpResultContextData
/// JSON, or a request that names neither a document nor a document ID.
/// </summary>
public const string RequestNotWellFormed = "HotChocolate.AspNetCore.RequestNotWellFormed";

/// <summary>
/// Marks the result of a request whose body exceeds the maximum request size.
/// </summary>
public const string RequestTooLarge = "HotChocolate.AspNetCore.RequestTooLarge";
}
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,9 @@ public static ParseRequestResult CreateEmptyRequestArrayResult(

/// <summary>
/// Creates the result for a request the parser rejected. The result of a document the
/// parser could not read carries a <c>400</c>, and the result of a request the parser read
/// but could not accept as a GraphQL over HTTP request is marked as not well-formed. The
/// parser could not read carries a <c>400</c>, the result of a request the parser read
/// but could not accept as a GraphQL over HTTP request is marked as not well-formed, and
/// the result of a request body over the maximum request size is marked as too large. The
/// exception the parser threw decides the kind, since an error filter may have rewritten
/// the errors that are written to the response.
/// </summary>
Expand All @@ -296,6 +297,12 @@ public static OperationResult CreateRequestErrorResult(
HttpResultContextData.RequestNotWellFormed,
null);
}
else if (exception.Errors is [{ Code: ErrorCodes.Server.MaxRequestSize }])
{
result.ContextData = result.ContextData.Add(
HttpResultContextData.RequestTooLarge,
null);
}

return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Net.Http.Json;
#endif
using System.Text;
using System.Text.Json;
using HotChocolate.AspNetCore.Formatters;
using HotChocolate.AspNetCore.Tests.Utilities;
using HotChocolate.Features;
Expand All @@ -25,7 +26,15 @@ public class GraphQLOverHttpSpecTests(TestServerFactory serverFactory) : ServerT
{
private static readonly Uri s_url = new("http://localhost:5000/graphql");
private static readonly HttpMethod s_queryMethod = new("QUERY");
private static readonly string s_oversizedRequest =
$$"""
{
"query": "{ __typename }",
"extensions": { "padding": "{{new string('a', MaxAllowedRequestSize)}}" }
}
""";

private const int MaxAllowedRequestSize = 256;
private const string NotWellFormedRequest = """{ "query": 123 }""";
private const string EmptyBatchRequest = "[]";
private const string NonObjectBatchRequest = "[1]";
Expand Down Expand Up @@ -812,6 +821,77 @@ public async Task Post_Should_ApplyContentTypeRule_When_PersistedDocumentCannotB
Assert.Equal(expectedContentType, response.Content.Headers.ContentType?.ToString());
}

// From the 2026-09-03 revision on, a request body over the maximum request size is answered
// 413.
[Theory]
[InlineData(null, Legacy, OK, ContentType.Json)]
[InlineData(null, Draft20250508, BadRequest, ContentType.GraphQLResponse)]
[InlineData(null, Draft20260903, RequestEntityTooLarge, ContentType.GraphQLResponse)]
[InlineData(ContentType.Json, Draft20250508, BadRequest, ContentType.Json)]
[InlineData(
ContentType.Json,
Draft20260903,
RequestEntityTooLarge,
ContentType.GraphQLResponse)]
public async Task Post_Should_ReturnContentTooLarge_When_BodyExceedsMaxRequestSize(
string? acceptHeader,
HttpTransportVersion transportVersion,
HttpStatusCode expectedStatusCode,
string expectedContentType)
{
// arrange
var client = GetSizeLimitedClient(transportVersion);

// act
using var request = new HttpRequestMessage(HttpMethod.Post, s_url);
request.Content = new StringContent(s_oversizedRequest, Encoding.UTF8, "application/json");
AddAcceptHeader(request, acceptHeader);

using var response = await client.SendAsync(request, TestContext.Current.CancellationToken);

// assert
Assert.Equal(expectedStatusCode, response.StatusCode);
Assert.Equal(expectedContentType, response.Content.Headers.ContentType?.ToString());
using var body = JsonDocument.Parse(
await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
body.MatchInlineSnapshot(
"""
{
"errors": [
{
"message": "Request size exceeds maximum allowed size.",
"extensions": {
"code": "HC0010"
}
}
]
}
""");
}

[Theory]
[InlineData(Draft20250508, BadRequest)]
[InlineData(Draft20260903, RequestEntityTooLarge)]
public async Task Post_Should_ReturnContentTooLarge_When_PersistedBodyExceedsMaxRequestSize(
HttpTransportVersion transportVersion,
HttpStatusCode expectedStatusCode)
{
// arrange
var client = GetSizeLimitedClient(transportVersion);

// act
using var request = new HttpRequestMessage(
HttpMethod.Post,
new Uri("http://localhost:5000/graphql/persisted/abc"));
request.Content = new StringContent(s_oversizedRequest, Encoding.UTF8, "application/json");

using var response = await client.SendAsync(request, TestContext.Current.CancellationToken);

// assert
Assert.Equal(expectedStatusCode, response.StatusCode);
Assert.Equal(ContentType.GraphQLResponse, response.Content.Headers.ContentType?.ToString());
}

[Theory]
[InlineData(Draft20250508, BadRequest)]
[InlineData(Draft20260903, UnprocessableContent)]
Expand Down Expand Up @@ -1861,6 +1941,27 @@ public async Task Query_Should_ReturnBadRequest_When_BodyHasNoJsonToken(
""");
}

[Theory]
[InlineData(Draft20250508, BadRequest)]
[InlineData(Draft20260903, RequestEntityTooLarge)]
public async Task Query_Should_ReturnContentTooLarge_When_BodyExceedsMaxRequestSize(
HttpTransportVersion transportVersion,
HttpStatusCode expectedStatusCode)
{
// arrange
var client = GetSizeLimitedClient(transportVersion);

// act
using var request = new HttpRequestMessage(s_queryMethod, s_url);
request.Content = new StringContent(s_oversizedRequest, Encoding.UTF8, "application/json");

using var response = await client.SendAsync(request, TestContext.Current.CancellationToken);

// assert
Assert.Equal(expectedStatusCode, response.StatusCode);
Assert.Equal(ContentType.GraphQLResponse, response.Content.Headers.ContentType?.ToString());
}

// A document that cannot be parsed in a QUERY request is answered on the same terms as one
// in a POST request.
[Theory]
Expand Down Expand Up @@ -2375,6 +2476,30 @@ private HttpClient GetQueryClient(HttpTransportVersion serverTransportVersion)
return server.CreateClient();
}

private HttpClient GetSizeLimitedClient(HttpTransportVersion serverTransportVersion)
{
var server = ServerFactory.Create(
services => services
.AddRouting()
.AddGraphQLServer(maxAllowedRequestSize: MaxAllowedRequestSize)
.AddQueryType(d => d.Field("greeting").Type<StringType>().Resolve("Hello"))
.AddHttpResponseFormatter(
new HttpResponseFormatterOptions
{
HttpTransportVersion = serverTransportVersion
}),
app => app
.UseRouting()
.UseEndpoints(
endpoints =>
{
endpoints.MapGraphQLPersistedOperations();
endpoints.MapGraphQL().WithOptions(o => o.EnableQueryRequests = true);
}));

return server.CreateClient();
}

private void AddAcceptHeader(HttpRequestMessage request, string? acceptHeader)
{
if (acceptHeader != null)
Expand Down
2 changes: 2 additions & 0 deletions website/content/docs/fusion/request-limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ The maximum HTTP request body size defaults to approximately 20 MB:
builder.AddGraphQLGateway(maxAllowedRequestSize: 5 * 1000 * 1024); // 5 MB
```

A JSON request over the limit is rejected before parsing with the error code `HC0010`. Under the `Draft20260903` [transport version](../hotchocolate/server/http-transport.md#transport-versions), the response has a `413` status code.

## Server Options

Control which HTTP methods and features are available:
Expand Down
2 changes: 1 addition & 1 deletion website/content/docs/hotchocolate/server/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ builder.AddGraphQL(

## maxAllowedRequestSize

Controls the maximum allowed size (in bytes) of an incoming GraphQL request body. The default is `20 * 1000 * 1024` (approximately 20 MB). If a request exceeds this limit, it is rejected before parsing.
Controls the maximum allowed size (in bytes) of an incoming GraphQL JSON request body. The default is `20 * 1000 * 1024` (approximately 20 MB). If a JSON request exceeds this limit, it is rejected before parsing with the error code `HC0010`. Under the `Draft20260903` [transport version](./http-transport.md#transport-versions), the response has a `413` status code.

Reduce this value if you expect only small queries and want to protect against excessively large payloads:

Expand Down
1 change: 1 addition & 0 deletions website/content/docs/hotchocolate/server/http-transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ A value outside this list throws an `ArgumentOutOfRangeException` when the forma
- An `application/json` response takes the status code of `application/graphql-response+json`, and only a `2xx` response carries `Content-Type: application/json`. Under `Draft20250508`, an `application/json` response has a `200` status code for every well-formed request and a `400` status code for a request the server cannot interpret.
- A result that carries both `data` and `errors` has a `294` status code. Under `Draft20250508`, it has a `200` status code.
- A request the server read but cannot execute has a `422` status code: a request that is not a well-formed GraphQL over HTTP request, a document that fails validation, an operation that cannot be determined, and variables that cannot be coerced. Under `Draft20250508`, these requests have a `400` status code for `application/graphql-response+json`; for `application/json`, only the request that is not well-formed has a `400` status code and the others have `200`. A request body that is not valid JSON has a `400` status code under both. A GraphQL document that cannot be parsed has a `400` status code under both for `application/graphql-response+json`, and a `200` status code under `Draft20250508` for `application/json`.
- A `POST` or `QUERY` request whose JSON body exceeds the [maximum request size](./endpoints.md#maxallowedrequestsize) has a `413` status code. Under `Draft20250508`, it has a `400` status code.
- A request on the GraphQL endpoint whose method the endpoint does not support has a `405` status code and an `Allow` header listing the supported methods, an `OPTIONS` request has a `204` status code with the same header, and a `POST` request whose `Content-Type` the endpoint does not support has a `415` status code. Under `Draft20250508`, all three have a `404` status code.
- When `EnableQueryRequests` is `true`, the `Allow` header lists `QUERY`, the `405`, `OPTIONS`, and `415` responses carry `Accept-Query: application/json`, and a `QUERY` request whose `Content-Type` the endpoint does not support has a `415` status code. Under `Draft20250508`, that request has a `404` status code.

Expand Down
Loading