Repository navigation
Conversation
A request with a multipart/form-data Content-Type and a zero-length body was rejected with `400 invalid body of multipart/form-data request` before any handler ran. The body really is not a valid MIME entity -- `multipart-body` in RFC 2046 5.1.1 requires a dash-boundary, one body-part and a close-delimiter, none of which a zero-length body has. But it is a valid HTTP request: RFC 9112 6 gives `message-body = *OCTET` and notes that request framing is independent of method semantics, and RFC 9113 8.1.1 does not list it among malformed HTTP/2 messages. No HTTP specification requires content to conform to the grammar of its declared media type. RFC 9110 2.4 leaves the choice to the application, so the parser now reports an empty body as an empty form and lets the handler decide whether a missing form is an error -- which is also what it already does for `--boundary--`, an empty form that spells itself out. The leniency is exactly zero-length: a Content-Type without `boundary`, a whitespace-only body and a truncated one all still fail.
Member
|
LGTM |
|
Many thanks for the PR! @apolukhin is now importing your pull request into our internal upstream repository. |
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.
closes: #1361
A request with a
multipart/form-dataContent-Typeand a zero-length body(
Content-Length: 0) is rejected with400 invalid body of multipart/form-data requestbefore any handler runs. Theparser now reports an empty body as an empty form and lets the handler decide.
Why
The honest version of this argument has two halves, because the input is
genuinely odd.
The content really is malformed.
RFC 2046 §5.1.1:
Only
*encapsulationis zero-or-more;dash-boundary, onebody-partandclose-delimiterare all mandatory, so a zero-length body cannot match. Noargument there.
But the request is valid HTTP, and that's what userver is answering.
RFC 9112 §6 gives
message-body = *OCTETand states that "request message framing is independentof method semantics".
RFC 9113 §8.1.1
enumerates malformed HTTP/2 messages exhaustively — extraneous frames,
prohibited or absent pseudo-header fields, invalid field names,
content-lengthnot matching the sum of DATA payload lengths — and a zero
content-lengthwithno DATA frames matches none of them.
content-typedoes not appear in thatdefinition at all. No HTTP specification requires content to conform to the
grammar of its declared media type.
So rejecting is permitted, not required —
RFC 9110 §2.4: "a
recipient MAY attempt to recover a usable protocol element from an invalid
construct. HTTP does not define specific error handling mechanisms except when
they have a direct impact on security, since different applications of the
protocol require different error handling strategies."
And if a server does reject,
§15.5.1 scopes 400 to
"malformed request syntax, invalid request message framing, or deceptive request
routing" — all HTTP-level concerns. A content-format problem has
415 and
422. So the current
400 is the wrong code even under the strictest reading.
userver already has the concept
A body of exactly
--boundary--— an empty form that spells itself out — isalready accepted with no form arguments (
ParseEmptyForm, andNoFinalCrLf2without the trailing CRLF). The parser therefore already has a "valid request,
no form arguments" outcome; the zero-byte case simply isn't routed to it. This PR
routes it there.
That also makes the current behaviour hard to defend as deliberate: two inputs
that mean the same thing to a handler get a 200 and a 400.
How
One early return at the top of
ParseMultipartFormDataBody. The RFC-citationcomment follows the existing convention in this file, which already annotates
ReadTokenandReadHeaderValuewith the grammar rule and link they implement.Handlers that require form arguments are unaffected:
GetFormDataArgreturns astatic empty
FormDataArgwhen the name is absent, so they can answer415/422/400 themselves with a message that describes their own contract.
What still fails
The leniency is exactly zero-length, nothing more:
multipart/form-datawith noboundary, empty body'boundary' parameter of multipart/form-data not found"\r\n"Unexpected request body end"--zzz\r\n"(truncated)All four are pinned by
ParseEmptyBodyLeniencyIsNarrow.Tests
ParseEmptyBody— an empty body parses to an empty form, for all threestrict_cr_lfsettings (default,true,false).ParseEmptyBodyLeniencyIsNarrow— the negative table above.test_empty_body_is_rejected_by_the_handlerinsamples/multipart_service— posting an empty body now gets the handler's ownExpecting PNG image formatinstead of the framework'sinvalid body of multipart/form-data request. The status is still 400; whatchanged is who chose it and what it says.
Verified locally:
ParseEmptyBodyand the functional test fail without theproduction change and pass with it;
ParseEmptyBodyLeniencyIsNarrowpasseseither way. Full core unit suite 2218/2219 (one pre-existing skipped death test)
and all 7
basic-chaossuites stay green.Relation to #1360
#1360 makes any
multipart parse failure non-fatal, which overlaps this at HTTP level: with that
merged, an empty body already reaches the handler. The two are still worth
having separately, and are independent commits on
develop:being an error at all — no warning logged, and
ParseMultipartFormData'scontract matches the spec reading above.
400.
Happy to land them in either order. Both touch
samples/multipart_service/tests/test_multipart.py, so whichever merges secondneeds a trivial rebase there — say the word and I'll do it.