Repository navigation
Fixed HTTP request body problems escaping as exceptions or echoing the body - #634
Open
xperiandri wants to merge 3 commits into
Open
xperiandri wants to merge 3 commits into
xperiandri wants to merge 3 commits into
Conversation
…e body The ASP.NET Core request handler turned several request body problems into unhandled exceptions (a 500 under most hosts) or into a 400 that repeated the whole request body back to the client: - A multipart body the form reader cannot parse (missing or unused boundary, truncated body, malformed part header) threw InvalidDataException or IOException out of HandleAsync. It is now answered with 400. - A body over Kestrel's MaxRequestBodySize threw BadHttpRequestException out of HandleAsync. Every BadHttpRequestException raised while reading the body is now answered with the status code the server gives (413 for a body that is too large). A body over a FormOptions limit, which the form reader reports as an InvalidDataException reading "<what> limit <number> exceeded.", is answered with 413. - A form request without an 'operations' field was answered with a 400 that repeated the whole multipart body, files included. It is now answered with a 400 that names the missing field. - The 'map' field was ignored. One that is not a JSON object of non-empty arrays of 'variables.' paths is now answered with 400. Its keys are not checked against the file fields, because the client of this library keys the map by index while it names the file fields by upload name. - The 400 response to a body that is not valid JSON read the whole body back into its 'received' member. That member is removed, and every reason quoted from the deserializer, the form reader or the server is cut to 256 characters, because such a reason can quote the body (the multipart reader repeats a malformed header line in its message). Every body problem keeps the existing problem details response format. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The form reader reports both a broken FormOptions limit and a malformed body with InvalidDataException, and any message containing " limit " and ending with " exceeded." was answered with 413. That also matched an over-long boundary parameter, a delimiter line followed by other text and a malformed part header, whose text the client controls. - Only the messages starting with the prefix of a size limit of FormOptions (multipart body, headers length or count, form value count, key or value length) are answered with 413; every other form reader problem with 400. - A body over FormOptions.BufferBodyLengthLimit, reported as an IOException "Buffer limit exceeded.", is answered with 413 instead of being described as a truncated multipart body. - A boundary too long for the buffer of the multipart reader, which only a raised MultipartBoundaryLengthLimit lets through, is answered with 400 instead of escaping as ArgumentOutOfRangeException, whose message repeats the boundary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
A valid overlong multipart header is incorrectly classified as 400 rather than the documented 413.
3 open findings
What changed in this PR
Improves ASP.NET Core request-body error handling to return safe 400/413 problem responses without echoing entire bodies.
Changes:
- Handles malformed, oversized, and invalid JSON/form bodies.
- Validates multipart operations and upload maps.
- Adds extensive request-body regression tests and release notes.
| File | Description |
|---|---|
RELEASE_NOTES.md |
Documents breaking response changes. |
GraphQLRequestHandler.fs |
Handles server body-read failures. |
HttpContext.fs |
Centralizes body parsing, validation, and errors. |
FSharp.Data.GraphQL.Tests.fsproj |
Includes the new test file. |
RequestBodyTests.fs |
Tests malformed and oversized bodies. |
RequestHandlerExtensibilityTests.fs |
Exposes reusable handler test setup. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| /// </remarks> | ||
| let private sizeLimitMessagePrefixes = [| | ||
| "Multipart body length limit " | ||
| "Multipart headers length limit " |
Comment on lines
+198
to
+200
| response | ||
| |> assertProblem StatusCodes.Status400BadRequest "Invalid multipart request" | ||
| response |> assertDoesNotEcho Marker |
Comment on lines
+423
to
+424
| response |> assertDoesNotEcho longHeaderLine | ||
| Assert.Contains (Marker, response.Body, StringComparison.Ordinal) |
… tests The tests take media types and header names from `MediaTypeNames` and `HeaderNames`, and the form field names and problem titles from the literals of `RequestBody`, instead of spelling them out again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Test Results 9 files 9 suites 14m 38s ⏱️ Results for commit 8d6be80. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Problem
The ASP.NET Core request handler turned request body problems into unhandled exceptions (a 500 under most hosts), or into a 400 that repeated the body back:
InvalidDataExceptionorIOExceptionout ofHandleAsync.MaxRequestBodySizethrewBadHttpRequestExceptionout ofHandleAsync.operationsfield: a form request without it got a 400 that repeated the whole multipart body, files included.receivedmember.Changes
MaxRequestBodySize: everyBadHttpRequestExceptiongets the status code the server gives;FormOptions: multipart body, headers length or count, value count, key or value length, and buffer.MultipartBoundaryLengthLimit. It now gets 400, instead of anArgumentOutOfRangeExceptionwhose message repeats the boundary.operationsfield: the 400 names the missing field.mapfield: it must be a JSON object of non-empty arrays ofvariables.paths, else 400. Its keys are not checked against the file fields, because the client of this library keys the map by index but names the file fields by upload name.received, and every reason quoted from the deserializer, the form reader or the server is cut to 256 characters.Tests
The tests are xUnit, in the new
RequestBodyTests.fsand inRequestHandlerExtensibilityTests.fs:operationsfield; invalid maps;Reverting the source makes them fail.
An independent review tried many more bodies in process and on a real Kestrel host. None gave a 500 or echoed the body or a file:
All 46 upload integration tests pass with the real client provider.
Verification:
Left for later
These are not changed here:
checkIfHasBodyjudges such a body by its length, which is 0 until it is read. A query sent after a middleware calledEnableBuffering()without reading the body gets the introspection result instead of being executed.HandleAsync,FormOptions.BufferBodyLengthLimitis never reached, because the handler buffers the body without a limit first.🤖 Generated with Claude Code