Parse multipart/form-data and forward uploaded files in LaravelHttpServer#223
Open
henryavila wants to merge 1 commit into
Open
Conversation
`LaravelHttpServer::handleRequest()` was passing `[]` to the `$files` argument of `Symfony\Component\HttpFoundation\Request::create()` with a `// @todo files...` placeholder. Any test that drove a real upload through Playwright (`->attach()`) would have the file written to the `<input>` and the `change` event dispatched correctly, but the Laravel kernel would never see the file: the existing `Tests/Browser/Webpage/ AttachTest.php` only asserts on `input.files[0].name` client-side, which masked the gap. This commit implements `parseMultipartBody()` invoked when the request content type starts with `multipart/form-data`. It is byte-safe (uses plain string functions, never `mb_*`) so binary uploads — .xlsx, .zip, images, PDFs — survive the parse without UTF-8 corruption. Each file part is written to a temp file in `sys_get_temp_dir()` and wrapped in a Symfony `UploadedFile` constructed with `$test: true` so it bypasses `is_uploaded_file()` (which only returns true for real SAPI uploads). PHP's bracket-notation for nested fields (`tags[]`, `user[name]`, `files[0]`) is honored for both parameters and files, delegating the nesting semantics to `parse_str()` via `http_build_query()`. `UploadedFile` instances are injected into the parsed structure after the round-trip since `parse_str()` only preserves scalars. Three new tests cover the end-to-end pipeline: - single file: client name + size + raw content visible server-side - binary content: bytes including 0xFF, 0xFE, 0xC0, 0x00, 0x0A, 0x0D survive intact (regression guard against any future mb_* usage) - mixed: file + non-file fields in the same request, both visible Real-world motivation: Filament v4 FileUpload (FilePond) + Livewire batch import flow could not be tested via this plugin because Livewire's `_finishUpload($name, $tmpPath = [], false)` received an empty array and threw `Undefined array key 0`. The FilePond JS bridge correctly POSTs the multipart payload; the server just dropped the file. This patch makes that pipeline testable end-to-end.
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.
Summary
LaravelHttpServer::handleRequest()was passing[]to the$filesargument ofSymfony\Component\HttpFoundation\Request::create()with a// @TODO files...placeholder. This means: any browser test that drives a real<input type=file>through Playwright ($page->attach(...)) has the file written to the input and thechangeevent fires correctly, but the Laravel kernel never sees the file —$request->file('avatar')returns null and$request->allFiles()is empty.The existing
tests/Browser/Webpage/AttachTest.phponly asserts ondocument.querySelector("input[name=avatar]").files[0].name(client-side DOM), which masked the gap: from a contributor's POV the API looks complete because the test passes, but no real Laravel upload pipeline can actually be exercised end-to-end.What this PR does
Implements
parseMultipartBody()inLaravelHttpServer. It is invoked when the requestContent-Typestarts withmultipart/form-data. The parser:explode/substr/strpos, nevermb_*) so binary uploads — .xlsx, .zip, images, PDFs — survive without UTF-8 corruption.Content-Dispositionto extract the field name and optionalfilename=.tempnam(sys_get_temp_dir(), 'pest_browser_upload_')and wrapped inSymfony\Component\HttpFoundation\File\UploadedFileconstructed with$test: true— Symfony then skipsis_uploaded_file(), which only returns true for real SAPI uploads.$parameters(so$request->input('note')works alongside$request->file('avatar')).tags[],user[name],files[0]) is honored by delegating toparse_str()viahttp_build_query();UploadedFileinstances are injected into the parsed structure after the round-trip (sinceparse_str()only preserves scalars).Wires the result into
Request::create()— replaces the[], // @TODO files...call site with the parsed$filesarray.multipart/form-datais now a recognized branch alongside the existingapplication/x-www-form-urlencodedparsing.Adds
tests/Browser/Webpage/AttachServerSideTest.php— three tests that close the gap:forwards a single uploaded file to the Laravel request— name + size + raw content visible server-side afterattach() + submit.preserves binary file content (no UTF-8 corruption)— bytes including0xFF,0xFE,0xC0,0x00,0x0A,0x0Dsurvive intact; regression guard against any futuremb_*usage in the parser.mixes file and non-file fields in the same multipart request—$request->file('avatar')and$request->input('note')both populated from the same POST.These tests use
fetch()for the form submit because they need to display the server's response in the same page (forassertSeeto work); the underlying multipart upload pipeline is identical to a native form submit.Real-world motivation
Filament v4 FileUpload (which wraps FilePond) + Livewire's batch upload pipeline could not be tested with this plugin because Livewire's
_finishUpload($name, $tmpPath = [], false)received an empty array and threwUndefined array key 0inWithFileUploads.php:49. After investigation, the FilePond JS bridge correctly POSTs the multipart payload — the server just dropped the file before the Laravel kernel ran. This patch makes that pipeline testable end-to-end.Test plan
vendor/bin/pest tests/Browser/Webpage/AttachServerSideTest.php— 3 new tests pass.vendor/bin/pest tests/Browser/Webpage/AttachTest.php— existing tests still pass (no regression).vendor/bin/pest tests/Browser/— full suite: 239 passed, 1 skipped (platform-specific, unrelated), 0 failed.Notes
sys_get_temp_dir()are not actively cleaned up by this patch. Symfony'sUploadedFileis constructed in test mode ($test: true) so move/store operations work normally; the OS reclaims/tmpon reboot, and individual test runs are short-lived. If you'd prefer explicit cleanup tied to the request lifecycle, happy to add it.parseMultipartBody()aims for correctness with the common shapes (name,name[],name[key],name[0]) rather than the exhaustive RFC 7578 surface (e.g.multipart/mixedfor arrays). Same scope as PHP's built-in SAPI parser for$_FILES. Happy to extend if needed.