What happened
attach() drives the browser correctly — Playwright really does put the file on the input, and the browser really does send a multipart/form-data POST — but the file never reaches the application. The Laravel test server discards it while rebuilding the request.
In src/Drivers/LaravelHttpServer.php::handleRequest() (5.x, and v5.0.1):
$parameters = [];
if ($method !== 'GET' && str_starts_with(mb_strtolower($contentType), 'application/x-www-form-urlencoded')) {
parse_str($rawBody, $parameters);
}
$symfonyRequest = Request::create(
$absoluteUrl,
$method,
$parameters,
$cookies,
[], // @TODO files...
$serverVariables,
$rawBody
);
Two things follow from that:
- Files are always empty — the 5th argument to
Request::create() is a hardcoded [].
multipart/form-data bodies are never parsed at all, so a multipart POST also loses its ordinary non-file fields, not just its files.
Reproducing it
Any Livewire component with a wire:model file input:
it('uploads a file', function () {
$path = sys_get_temp_dir().'/probe.pdf';
file_put_contents($path, "%PDF-1.4\n");
visit('/some-page-with-an-upload')
->attach('input[type="file"]', $path)
->assertSee('probe');
});
Livewire fatals server-side the moment it tries to finish the upload:
ErrorException: Undefined array key 0
vendor/livewire/livewire/src/Features/SupportFileUploads/WithFileUploads.php:60
App\Livewire\SomeComponent->_finishUpload('attachment', Array, false, false)
Environment
pestphp/pest-plugin-browser v5.0.1 (latest release)
pestphp/pest 5.1.1, PHPUnit 13.3, Laravel 13.25, Livewire 4.4, PHP 8.5
- Chromium via Playwright 1.62.1
Still present on the 5.x default branch as of today, so this is not fixed in unreleased work either.
Why it's worth reporting rather than working around
attach() is a public, documented-by-existence part of the API, and it fails silently from the caller's point of view — the browser side succeeds, so the failure surfaces much later as an unrelated-looking server exception. It took a while to trace back to the harness rather than to our own code.
It also rules out browser-testing any upload surface in an application, which for anything with a file-handling flow is a fairly large hole in what the tier can cover.
Offer of a PR
Happy to put a PR together, but there's one design question I'd rather ask than guess, because it affects the shape of the fix.
PHP has no built-in parser for a multipart/form-data body that didn't arrive through the SAPI — $_FILES is only populated for real requests, and Request::create() expects already-parsed UploadedFile instances. So the fix needs either:
amphp/http-server-form-parser, which is the natural fit given the plugin already depends on amphp/http-server — but it isn't currently in the tree, so this adds a dependency; or
- a hand-rolled boundary parser inside the driver, which avoids the dependency but is more code and easy to get subtly wrong on edge cases (nested field names, filenames with quotes, multiple files per field).
Either way the shape is the same:
parse the multipart body → write each part to a temp file
→ build Symfony UploadedFile instances
→ pass them as the 5th argument
→ populate $parameters from the non-file parts too
Which of those two would you accept? I'll write whichever you prefer.
What happened
attach()drives the browser correctly — Playwright really does put the file on the input, and the browser really does send amultipart/form-dataPOST — but the file never reaches the application. The Laravel test server discards it while rebuilding the request.In
src/Drivers/LaravelHttpServer.php::handleRequest()(5.x, and v5.0.1):Two things follow from that:
Request::create()is a hardcoded[].multipart/form-databodies are never parsed at all, so a multipart POST also loses its ordinary non-file fields, not just its files.Reproducing it
Any Livewire component with a
wire:modelfile input:Livewire fatals server-side the moment it tries to finish the upload:
Environment
pestphp/pest-plugin-browserv5.0.1 (latest release)pestphp/pest5.1.1, PHPUnit 13.3, Laravel 13.25, Livewire 4.4, PHP 8.5Still present on the
5.xdefault branch as of today, so this is not fixed in unreleased work either.Why it's worth reporting rather than working around
attach()is a public, documented-by-existence part of the API, and it fails silently from the caller's point of view — the browser side succeeds, so the failure surfaces much later as an unrelated-looking server exception. It took a while to trace back to the harness rather than to our own code.It also rules out browser-testing any upload surface in an application, which for anything with a file-handling flow is a fairly large hole in what the tier can cover.
Offer of a PR
Happy to put a PR together, but there's one design question I'd rather ask than guess, because it affects the shape of the fix.
PHP has no built-in parser for a
multipart/form-databody that didn't arrive through the SAPI —$_FILESis only populated for real requests, andRequest::create()expects already-parsedUploadedFileinstances. So the fix needs either:amphp/http-server-form-parser, which is the natural fit given the plugin already depends onamphp/http-server— but it isn't currently in the tree, so this adds a dependency; orEither way the shape is the same:
Which of those two would you accept? I'll write whichever you prefer.