Skip to content

fix: parse multipart/form-data request bodies in LaravelHttpServer - #252

Open
Llewdur wants to merge 1 commit into
pestphp:4.xfrom
Llewdur:fix/multipart-form-data-parsing
Open

fix: parse multipart/form-data request bodies in LaravelHttpServer#252
Llewdur wants to merge 1 commit into
pestphp:4.xfrom
Llewdur:fix/multipart-form-data-parsing

Conversation

@Llewdur

@Llewdur Llewdur commented Aug 21, 2026

Copy link
Copy Markdown

Summary

LaravelHttpServer::handleRequest() only parses
application/x-www-form-urlencoded bodies. A multipart request — the
shape a real <input type="file"> produces when submitted through the
browser — reaches the in-process test server with its body untouched,
and Request::create() is always called with [] for its $files
argument (the // @TODO files...). Concretely: any Laravel app testing
a file upload through visit() gets a 422 from its Form Request,
because the file never arrives.

This PR adds Support\MultipartFormDataParser, a small, dependency-free
parser that turns a raw multipart/form-data body into the
[$parameters, $files] shape Symfony\Component\HttpFoundation\Request::create()
expects, and wires it into handleRequest() alongside the existing
urlencoded branch.

Details

  • Text fields are parsed into $parameters, uploaded parts are written
    to a temp file and described in the same $_FILES-style array PHP's
    own SAPI produces (name/type/tmp_name/error/size).
  • Bracketed field names (documents[], meta[address]) are supported
    via a small recursive path-setter that mirrors how PHP itself parses
    bracketed form field names — this covers both single- and
    multi-file inputs.
  • An empty file input (no file chosen) is reported as
    UPLOAD_ERR_NO_FILE, matching what a real PHP SAPI does, rather than
    being silently dropped or treated as a zero-byte file.
  • Symfony\Component\HttpFoundation\FileBag::fixPhpFilesArray()
    already normalizes either the "normal" nested shape or PHP's own
    quirky flattened $_FILES shape, so the parser only needs to build
    the straightforward nested one.

Testing

Unit tests are included at tests/Unit/Support/MultipartFormDataParserTest.php
covering: plain text fields (including multi-line values), a single
file upload, an empty file input, and repeated bracketed fields
(tags[], documents[]) collecting into a list for both text and file
parts.

I wasn't able to run this repo's own suite locally (the sandbox I'm
working from is missing ext-dom, which pestphp/pest itself needs to
boot), so I verified the parser two ways instead, both passing:

  1. A standalone script exercising every case in the test file directly
    against MultipartFormDataParser::parse().
  2. The same output round-tripped through the real
    Symfony\Component\HttpFoundation\Request::create() +
    FileBag — confirming a bracketed multi-file field converts into a
    list of genuine UploadedFile instances with the correct original
    names and on-disk content, i.e. exactly what handleRequest() does
    with the parser's output.

Happy to adjust anything CI flags once it runs, or to add a visit()-
level Browser test exercising an actual <input type="file"> if that's
preferred over (or in addition to) the unit tests.

Related

We hit this while trying to write a browser test for a document-upload
flow in our own app and found the // @TODO files... — this closes that
gap for us and (hopefully) for anyone else hitting the same 422.

`handleRequest()` only understood `application/x-www-form-urlencoded`
bodies, so a real file upload driven through the browser (a `<input
type="file">` set via a `File`/`DataTransfer`, or any `FormData` submit)
reached the in-process test server as an unparsed multipart body: no
fields, no files, and `Request::create()` was always given `[]` for its
files argument (the `// @todo files...`).

Add `Support\MultipartFormDataParser`, a small, framework-agnostic
parser that turns a raw multipart body into the `[$parameters, $files]`
shape `Symfony\Component\HttpFoundation\Request::create()` expects.
Uploaded parts are written to temp files and described in the same
`$_FILES`-style array PHP itself produces, including for bracketed
field names (`documents[]`) - `FileBag::fixPhpFilesArray()` normalizes
either shape, so this doesn't need to special-case it. An empty file
input (no file chosen) is reported as `UPLOAD_ERR_NO_FILE`, matching a
real PHP SAPI.

Verified directly against `symfony/http-foundation`'s `Request::create()`
and `FileBag`: bracketed multi-file fields convert into a list of real
`UploadedFile` instances with the right original names and on-disk
content.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant