vapor/vapor

Unprocessable Entity status code when decoding empty Request bodies

vzsg opened this issue · 2 comments

vzsg commented

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

  1. Start a Vapor project, e.g. by cloning template-bare.

  2. 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")'"
    }
    
  3. Start the application and navigate to http://localhost:8080/form.

  4. Select one or more car makes, press Submit.

  5. Observe that the response contains Received cars: '(comma-separated strings)'.

  6. Navigate back to the form and reload the page to clear the selection.

  7. Now, without selecting any of the makes, press Submit.

  8. 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?

vzsg commented

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.