From a10a38d5d080f7e50d868624e4234d259efb5528 Mon Sep 17 00:00:00 2001 From: Glen Date: Mon, 28 Sep 2026 10:43:01 +0200 Subject: [PATCH 1/2] Answer 413 for an oversized request body under Draft20260903 --- .../DefaultHttpResponseFormatter.cs | 20 ++- .../Parsers/DefaultHttpRequestParser.cs | 2 +- .../AspNetCorePipelineResources.resx | 2 +- .../Utilities/HttpResultContextData.cs | 5 + .../Utilities/MiddlewareHelper.cs | 11 +- .../GraphQLOverHttpSpecTests.cs | 125 ++++++++++++++++++ website/content/docs/fusion/request-limits.md | 2 + .../docs/hotchocolate/server/endpoints.md | 2 +- .../hotchocolate/server/http-transport.md | 1 + 9 files changed, 161 insertions(+), 9 deletions(-) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultHttpResponseFormatter.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultHttpResponseFormatter.cs index d9f69ed912c..f5390270076 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultHttpResponseFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultHttpResponseFormatter.cs @@ -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; @@ -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; @@ -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; } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/DefaultHttpRequestParser.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/DefaultHttpRequestParser.cs index dc9798200ff..956385ac3f3 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/DefaultHttpRequestParser.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/DefaultHttpRequestParser.cs @@ -165,7 +165,7 @@ private async ValueTask 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) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Properties/AspNetCorePipelineResources.resx b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Properties/AspNetCorePipelineResources.resx index 580230b7a1b..a15a46bd737 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Properties/AspNetCorePipelineResources.resx +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Properties/AspNetCorePipelineResources.resx @@ -25,7 +25,7 @@ Either the parameter query or the parameter id has to be set. - Max GraphQL request size reached. + Request size exceeds maximum allowed size. The response type is not supported. diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/HttpResultContextData.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/HttpResultContextData.cs index 121d752b51c..f6d3c5d05a3 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/HttpResultContextData.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/HttpResultContextData.cs @@ -12,4 +12,9 @@ internal static class HttpResultContextData /// JSON, or a request that names neither a document nor a document ID. /// public const string RequestNotWellFormed = "HotChocolate.AspNetCore.RequestNotWellFormed"; + + /// + /// Marks the result of a request whose body exceeds the maximum request size. + /// + public const string RequestTooLarge = "HotChocolate.AspNetCore.RequestTooLarge"; } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/MiddlewareHelper.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/MiddlewareHelper.cs index 9c18c0b0085..0a25208e40f 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/MiddlewareHelper.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/MiddlewareHelper.cs @@ -273,8 +273,9 @@ public static ParseRequestResult CreateEmptyRequestArrayResult( /// /// Creates the result for a request the parser rejected. The result of a document the - /// parser could not read carries a 400, 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 400, 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. /// @@ -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; } diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs index 338d6a53897..e0e4957ceeb 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs @@ -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; @@ -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]"; @@ -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)] @@ -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] @@ -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().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) diff --git a/website/content/docs/fusion/request-limits.md b/website/content/docs/fusion/request-limits.md index 8c5e8bdb30b..4e0563dc9b4 100644 --- a/website/content/docs/fusion/request-limits.md +++ b/website/content/docs/fusion/request-limits.md @@ -167,6 +167,8 @@ The maximum HTTP request body size defaults to approximately 20 MB: builder.AddGraphQLGateway(maxAllowedRequestSize: 5 * 1000 * 1024); // 5 MB ``` +A 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: diff --git a/website/content/docs/hotchocolate/server/endpoints.md b/website/content/docs/hotchocolate/server/endpoints.md index 0dc1bd41d0f..0e28aa2b49b 100644 --- a/website/content/docs/hotchocolate/server/endpoints.md +++ b/website/content/docs/hotchocolate/server/endpoints.md @@ -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 request body. The default is `20 * 1000 * 1024` (approximately 20 MB). If a 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: diff --git a/website/content/docs/hotchocolate/server/http-transport.md b/website/content/docs/hotchocolate/server/http-transport.md index aecc40d926d..f05bc6ab093 100644 --- a/website/content/docs/hotchocolate/server/http-transport.md +++ b/website/content/docs/hotchocolate/server/http-transport.md @@ -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. From 687e528dbfa5f0c22dfeaf82c516a2b971d0262c Mon Sep 17 00:00:00 2001 From: Glen Date: Mon, 28 Sep 2026 10:58:54 +0200 Subject: [PATCH 2/2] [Website] Scope the request size limit docs to JSON requests --- website/content/docs/fusion/request-limits.md | 2 +- website/content/docs/hotchocolate/server/endpoints.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/content/docs/fusion/request-limits.md b/website/content/docs/fusion/request-limits.md index 4e0563dc9b4..14b10e4b4ee 100644 --- a/website/content/docs/fusion/request-limits.md +++ b/website/content/docs/fusion/request-limits.md @@ -167,7 +167,7 @@ The maximum HTTP request body size defaults to approximately 20 MB: builder.AddGraphQLGateway(maxAllowedRequestSize: 5 * 1000 * 1024); // 5 MB ``` -A 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. +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 diff --git a/website/content/docs/hotchocolate/server/endpoints.md b/website/content/docs/hotchocolate/server/endpoints.md index 0e28aa2b49b..fd355cbc849 100644 --- a/website/content/docs/hotchocolate/server/endpoints.md +++ b/website/content/docs/hotchocolate/server/endpoints.md @@ -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 with the error code `HC0010`. Under the `Draft20260903` [transport version](./http-transport.md#transport-versions), the response has a `413` status code. +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: