Unprocessable Entity status code when decoding empty Request bodies
vzsg opened this issue · 2 comments
Describe the bug
Given a POST route handler that expects an URL-encoded form body, when a client sends an empty request body, calling Request.content.decode(...)
causes a 422 Unprocessable Entity abort to be thrown.
This is an issue, because browsers typically omit empty values when submitting a HTML form without JavaScript being involved. If every input is on their default/empty value, this means the request body could become an empty string.
In other words, a form body with Content-Length: 0
is valid, but throws an obscure error now.
To Reproduce
-
Start a Vapor project, e.g. by cloning template-bare.
-
Add the following two route handlers:
app.get("form") { req -> Response in let body = """ <html> <head></head> <body> <form method="POST"> <select name="cars[]" id="cars" multiple> <option value="volvo">Volvo</option> <option value="saab">Saab</option> <option value="opel">Opel</option> <option value="audi">Audi</option> </select> <input type="submit"/> </form> </body> </html> """ let res = Response(body: Response.Body(string: body)) res.headers.contentType = .html return res } app.post("form") { req -> String in struct FormData: Content { var cars: [String]? } let data = try req.content.decode(FormData.self) return "Received cars: '\(data.cars?.joined(separator: ", ") ?? "NULL")'" }
-
Start the application and navigate to
http://localhost:8080/form
. -
Select one or more car makes, press Submit.
-
Observe that the response contains
Received cars: '(comma-separated strings)'
. -
Navigate back to the form and reload the page to clear the selection.
-
Now, without selecting any of the makes, press Submit.
-
Observe a 422 error is returned.
Expected behavior
The decode
call should succeed, return a FormData struct with a nil array inside, and ultimately the route should return the string Received cars: 'NULL'
.
Environment
- Vapor Framework version: 4.77.0
- Vapor Toolbox version: irrelevant
- OS version: macOS Ventura 13.4.1 (but also irrelevant)
- Browser: Chrome 114
Additional context
I did some debugging, originally thinking that the issue is somewhere inside URLEncodedFormDecoder. That was ultimately a bust, execution does not reach the decoder class at all! Instead, the error is thrown from here. In case of Content-Length: 0
, the bodyStorage
is none
, which triggers this check.
Workaround
Adding a hidden input with an arbitrary (ignored) value will cause the browser to always include this key-value pair in the body, so it is no longer empty even if the user-facing inputs are, avoiding the 422 error.
<input type="hidden" name="_foo" value="foo"/>
Hidden inputs are pretty common for CSRF and other purposes, which might be why this issue went unnoticed for so long.
I think this a same issue I’ve faced a while back.
#2606
But it’s closed.
I was thinking if a body contents is nil, can it be replaced with an empty body contents, that validations can be run, so the correct validation error thrown?
That indeed looks like the same issue, as validations also go through Content (Codable).
I'm more and more inclined to think that the empty body check is just wrong.