Repository navigation
Conversation
userver parsed the multipart/form-data body of every request whose Content-Type says so, whether or not the handler ever reads form arguments, and turned any parse failure into `400 invalid body of multipart/form-data request` before the handler ran. There was no way to opt out. Both are wrong defaults. RFC 9110 2.4 leaves error handling for an invalid construct to the application, and 15.5.1 scopes 400 to malformed request syntax, invalid framing and deceptive routing -- a body that does not match its declared media type is a content-format problem, which has 415 and 422. Only the handler knows whether a missing or malformed form is fatal to the operation being requested. A parse failure is now logged and the handler is handed an empty form, so it can answer with a status that describes its own contract. Arguments the parser filled in before failing partway are dropped rather than exposed: "empty form" is a semantic worth documenting, "arbitrary prefix of a form" is not. Status::kParseMultipartFormDataError becomes unreachable and is removed. No test asserted the framework's 400.
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: #1359
userver parses the
multipart/form-databody of every request whoseContent-Typesays so, whether or not the handler ever reads form arguments,and turns any parse failure into
400 invalid body of multipart/form-data requestbefore the handler runs. There is no way to opt out.With this change a parse failure is logged and the handler is handed an empty
form, so the handler decides what the response should be.
Why this is the framework's call to stop making
RFC 9110 §2.4: "Unless
noted otherwise, 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."
That is explicitly an application-level choice, and the request constructor is
not the application. Only the handler knows whether a missing or malformed form
is fatal to the operation being requested — a handler that never looks at form
arguments has no reason to fail at all.
And when 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 body that does not match its
declared media type is a content-format problem, and HTTP has
415 and
422 for exactly that.
So the status is wrong as well as misplaced — but picking 415 in the framework
would only make it wrong more precisely. The fix is to stop choosing.
The inconsistency this removes
The block immediately above the multipart one does the same category of work
with the opposite default:
:209-220)parse_args_from_bodykParseArgsError:222-231)bool→kParseMultipartFormDataErrorTwo adjacent blocks, two conventions. This moves multipart to the more
conservative one: parsing never decides the response.
Why no config option
An earlier draft added a per-handler option selecting the behaviour, defaulting
to today's 400. That shape is wrong:
must discover and set an option just to stop the framework returning a wrong
status on its behalf. Nobody goes looking for a flag to fix behaviour they
assume is already right.
parse_args_from_body, oneboolin this subsystem costs ~13 files: the struct field, the YAML→structparse, a JSON-schema declaration for strict static validation, all duplicated
across
handler-defaultsand the per-handler level, a copy into theper-request config, two positional aggregate initialisers, and the doc-sample
config. That is a lot of surface to add speculatively.
preserve compatibly.
If back-compat turns out to matter for a service someone can name, I'd rather add
an opt-out that restores the legacy 400, defaulting to the new behaviour,
than an opt-in to correctness — and add it during review, when the need is
concrete, rather than building the plumbing up front.
The current behaviour is not contract
kParseMultipartFormDataErrorexists in exactly three places — the enum value,the one
SetStatuscall, and the oneCheckStatuscase that maps it to 400.Nothing in the test suite asserts it: no unit test, no functional test, no
testsuite test. The string
invalid body of multipart/form-data requestappearsonly at its single production site.
samples/multipart_service'stest_bad_content_typeasserts the handler's 400, not the framework's.So the status becomes unreachable and this PR removes it.
How
Plus removal of the now-unreachable enum value and its
CheckStatuscase.Three deliberate details:
SetFormDataArgsis not called on failure. The parser does fill inarguments before failing partway —
ParseMultipartFormDataValueinserts perpart and a later failure returns
falsewithout clearing, and thedefault-charset fixup only runs on the success path. "Empty form" is a semantic
worth documenting; "arbitrary prefix of a form, with charsets possibly unset"
is not.
LOG_LIMITED_WARNING, notLOG_WARNING. The trigger isattacker-controlled request content, so an unthrottled line per request is a
log-flooding vector. The parser already logs the specific reason; this line adds
the consequence.
HttpRequestImpl::form_data_argsis a default-constructed map andGetFormDataArgreturns a static emptyFormDataArgwhen the name is absent,so never calling
SetFormDataArgsis safe by construction — the same state aplain
GETalready reaches the handler in.Tests
Four functional tests. This behaviour is only observable at the HTTP level,
because the question is who chose the status.
New
core/functional_tests/basic_chaos/tests-nonchaos/handlers/test_multipart_parse_failure.py,against
/chaos/httpserver?type=echo— a handler that echoes the body and nevertouches form args:
test_malformed_multipart_body_reaches_handler— a garbage body with amultipart/form-datacontent type now gets the handler's own 200, body echoed.test_empty_multipart_body_reaches_handler— same for a zero-length body.samples/multipart_service/tests/test_multipart.py, against a handler that doesread form args:
test_malformed_body_is_rejected_by_the_handler— still 400, but the body isnow the handler's
Expecting PNG image formatrather than the framework'sinvalid body of multipart/form-data request. Same status, different decider.test_partially_parsed_body_exposes_no_args— first part parses cleanly(
profileImagewith the PNG magic bytes), second part is truncated. If partialargs leaked, the handler would clear its PNG check and then 500 on
formats::json::FromString("")for the missingaddress; asserting the 400pins that they do not.
All four fail without the production change and pass with it. No existing test
needed changing; all 7
basic-chaossuites and 2216 core unit tests stay green.Relation to
// TODO: split logicThe
// TODO: split logicon:222sits exactly on the boundary this PR moves.cbb7230fc("cc http: move out HttpRequestImplBuilder") moved cookie parsinginto
HttpRequestBuilder::Build(), whereParseCookies()returnsvoidandcannot fail, and left query/urlencoded args and multipart behind in the
constructor with two different failure conventions.
This PR is a step in the direction the TODO points — parsing stops deciding the
response — but not the whole split. The larger refactor (parsers return results;
one place maps result → status; or interpretation moves to first access in
GetArg/GetFormDataArg) can follow independently, so I've left the TODO inplace.
One visible leftover I noticed but deliberately did not touch here:
kParseCookiesErroris still declared and still has a liveCheckStatuscaseproducing
400 invalid cookies, but nothing sets it — it went dead when cookieparsing moved into the builder. Happy to send that as a separate one-line
cleanup; it didn't belong in a PR that changes a default.