Skip to content

Commit 0da8f00

Browse files
authored
Return 400 on JSON parse failure for application/json (#9784)
1 parent 88caeb7 commit 0da8f00

4 files changed

Lines changed: 48 additions & 9 deletions

File tree

dictionary.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ Trimmable
282282
Tzdb
283283
unittests
284284
unlisten
285+
unparseable
285286
unpublish
286287
unserialized
287288
Unsubscriber

src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultHttpResponseFormatter.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class DefaultHttpResponseFormatter : IHttpResponseFormatter
3434
private readonly FormatInfo _eventStreamFormat;
3535
private readonly FormatInfo _jsonLinesFormat;
3636
private readonly FormatInfo _legacyFormat;
37+
private readonly bool _isLegacyTransport;
3738
private readonly IncrementalDeliveryFormat _incrementalDeliveryDefaultFormat;
3839

3940
/// <summary>
@@ -122,7 +123,8 @@ public DefaultHttpResponseFormatter(
122123
ContentType.JsonLines,
123124
ResponseContentType.JsonLines,
124125
jsonLinesResultFormatter);
125-
_defaultFormat = options.HttpTransportVersion is HttpTransportVersion.Legacy
126+
_isLegacyTransport = options.HttpTransportVersion is HttpTransportVersion.Legacy;
127+
_defaultFormat = _isLegacyTransport
126128
? _legacyFormat
127129
: _graphqlResponseFormat;
128130

@@ -400,11 +402,23 @@ protected virtual HttpStatusCode OnDetermineStatusCode(
400402
FormatInfo format,
401403
HttpStatusCode? proposedStatusCode)
402404
{
403-
// the current spec proposal strongly recommends to always return OK
404-
// when using the legacy application/json response content-type.
405405
if (format.Kind is ResponseContentType.Json)
406406
{
407-
return HttpStatusCode.OK;
407+
// the legacy transport preserves the pre-spec behavior of always returning
408+
// 200 for the application/json response content-type.
409+
if (_isLegacyTransport)
410+
{
411+
return HttpStatusCode.OK;
412+
}
413+
414+
// per graphql-over-http §6.4.1, the application/json response content-type
415+
// should return 200 for every well-formed request regardless of errors
416+
// raised. the only 4xx is 400 for requests the server cannot interpret
417+
// (§6.4.1.1.1 JSON parse, §6.4.1.1.2 invalid parameters). honor a proposed
418+
// 400; everything else, including an unexpected 500, stays 200.
419+
return proposedStatusCode is HttpStatusCode.BadRequest
420+
? HttpStatusCode.BadRequest
421+
: HttpStatusCode.OK;
408422
}
409423

410424
// if we are sending a single result with the multipart/mixed header or

src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpPostMiddlewareBase.cs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,16 @@ protected async Task HandleRequestAsync(HttpContext context, ExecutorSession ses
9797
}
9898
catch (GraphQLRequestException ex)
9999
{
100-
// A GraphQL request exception is thrown if the HTTP request body couldn't be
101-
// parsed. In this case we will return HTTP status code 400 and return a
102-
// GraphQL error result.
103-
statusCode = HttpStatusCode.BadRequest;
100+
// request-interpretation failures (invalid JSON, missing query, etc.) propose
101+
// 400 per graphql-over-http §6.4.1.1.1. GraphQL document syntax errors
102+
// are document-parsing failures and leave the proposed status unset so the
103+
// formatter applies the per-content-type rule (200 for application/json
104+
// per §6.4.1.1.3, 400 for application/graphql-response+json).
105+
//
106+
// classification uses the original parser errors: error filters run by
107+
// session.Handle can rewrite the error code and would otherwise misclassify
108+
// the failure. the handled errors are used only for the response body.
109+
statusCode = IsDocumentSyntaxError(ex.Errors) ? null : HttpStatusCode.BadRequest;
104110
var errors = session.Handle(ex.Errors);
105111
result = OperationResult.FromError([.. errors]);
106112
session.DiagnosticEvents.ParserErrors(context, errors);
@@ -261,6 +267,24 @@ await session.ParseRequestAsync(
261267
return requests;
262268
}
263269

270+
private static bool IsDocumentSyntaxError(IReadOnlyList<IError> errors)
271+
{
272+
if (errors.Count == 0)
273+
{
274+
return false;
275+
}
276+
277+
for (var i = 0; i < errors.Count; i++)
278+
{
279+
if (!string.Equals(errors[i].Code, ErrorCodes.Server.SyntaxError, StringComparison.Ordinal))
280+
{
281+
return false;
282+
}
283+
}
284+
285+
return true;
286+
}
287+
264288
private static bool TryParseOperations(
265289
string operationNameString,
266290
[NotNullWhen(true)] out IReadOnlyList<string>? operationNames)

src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ public async Task SingleResult_MultipartAcceptHeader(string acceptHeader)
121121
[InlineData("*/*", Legacy, OK, ContentType.Json)]
122122
[InlineData("application/*", Latest, BadRequest, ContentType.GraphQLResponse)]
123123
[InlineData("application/*", Legacy, OK, ContentType.Json)]
124-
[InlineData(ContentType.Json, Latest, OK, ContentType.Json)]
124+
[InlineData(ContentType.Json, Latest, BadRequest, ContentType.Json)]
125125
[InlineData(ContentType.Json, Legacy, OK, ContentType.Json)]
126126
[InlineData(ContentType.GraphQLResponse, Latest, BadRequest, ContentType.GraphQLResponse)]
127127
[InlineData(ContentType.GraphQLResponse, Legacy, BadRequest, ContentType.GraphQLResponse)]

0 commit comments

Comments
 (0)