Skip to content

[Browser] attach() uploads never reach the application — LaravelHttpServer discards files and does not parse multipart bodies #1873

Description

@stevewolfe555

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:

  1. Files are always empty — the 5th argument to Request::create() is a hardcoded [].
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions