Skip to content

Commit 6e72b2e

Browse files
committed
fix: improve multipart file parsing and upload error handling in LaravelHttpServer
1 parent 90a5998 commit 6e72b2e

File tree

4 files changed

+548
-187
lines changed

4 files changed

+548
-187
lines changed

src/Drivers/LaravelHttpServer.php

Lines changed: 16 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
use Amp\ByteStream\ReadableResourceStream;
88
use Amp\Http\Cookie\RequestCookie;
99
use Amp\Http\Server\DefaultErrorHandler;
10-
use Amp\Http\Server\FormParser\BufferedFile;
11-
use Amp\Http\Server\FormParser\Form;
1210
use Amp\Http\Server\HttpServer as AmpHttpServer;
1311
use Amp\Http\Server\HttpServerStatus;
1412
use Amp\Http\Server\Request as AmpRequest;
@@ -19,13 +17,13 @@
1917
use Illuminate\Contracts\Http\Kernel as HttpKernel;
2018
use Illuminate\Foundation\Testing\Concerns\WithoutExceptionHandlingHandler;
2119
use Illuminate\Http\Request;
22-
use Illuminate\Http\UploadedFile;
2320
use Illuminate\Routing\UrlGenerator;
2421
use Illuminate\Support\Uri;
2522
use Pest\Browser\Contracts\HttpServer;
2623
use Pest\Browser\Exceptions\ServerNotFoundException;
2724
use Pest\Browser\Execution;
2825
use Pest\Browser\GlobalState;
26+
use Pest\Browser\Http\ExtendedFormParser;
2927
use Pest\Browser\Playwright\Playwright;
3028
use Psr\Log\NullLogger;
3129
use Symfony\Component\Mime\MimeTypes;
@@ -53,14 +51,19 @@ final class LaravelHttpServer implements HttpServer
5351
*/
5452
private ?Throwable $lastThrowable = null;
5553

54+
/**
55+
* The multipart parser wrapper with upload validation behavior.
56+
*/
57+
private ExtendedFormParser $extendedFormParser;
58+
5659
/**
5760
* Creates a new laravel http server instance.
5861
*/
5962
public function __construct(
6063
public readonly string $host,
6164
public readonly int $port,
6265
) {
63-
//
66+
$this->extendedFormParser = ExtendedFormParser::fromIni();
6467
}
6568

6669
/**
@@ -72,6 +75,14 @@ public function __destruct()
7275
// $this->stop();
7376
}
7477

78+
/**
79+
* Overrides the multipart parser instance.
80+
*/
81+
public function setExtendedFormParser(ExtendedFormParser $extendedFormParser): void
82+
{
83+
$this->extendedFormParser = $extendedFormParser;
84+
}
85+
7586
/**
7687
* Rewrite the given URL to match the server's host and port.
7788
*/
@@ -383,170 +394,6 @@ private function rewriteAssetUrl(string $content): string
383394
*/
384395
private function parseMultipartFormData(AmpRequest $request): array
385396
{
386-
$form = Form::fromRequest($request);
387-
388-
$values = $form->getValues();
389-
$files = $form->getFiles();
390-
391-
return [
392-
$this->normalizeMultipartParameters($values),
393-
$this->normalizeMultipartFiles($files),
394-
];
395-
}
396-
397-
/**
398-
* Normalize multipart field values to a Symfony request-compatible array.
399-
*
400-
* @param array<string, list<string>> $fields
401-
* @return array<int|string, mixed>
402-
*/
403-
private function normalizeMultipartParameters(array $fields): array
404-
{
405-
$normalized = [];
406-
407-
foreach ($fields as $field => $values) {
408-
foreach ($values as $value) {
409-
$this->setFieldValue($normalized, $field, $value);
410-
}
411-
}
412-
413-
return $normalized;
414-
}
415-
416-
/**
417-
* Normalize multipart files to a Symfony request-compatible files array.
418-
*
419-
* @param array<string, list<BufferedFile>> $files
420-
* @return array<int|string, mixed>
421-
*/
422-
private function normalizeMultipartFiles(array $files): array
423-
{
424-
$normalized = [];
425-
426-
foreach ($files as $field => $fileEntries) {
427-
foreach ($fileEntries as $fileEntry) {
428-
$this->setFieldValue($normalized, $field, $this->createUploadedFile($fileEntry));
429-
}
430-
}
431-
432-
return $normalized;
433-
}
434-
435-
/**
436-
* @param array<int|string, mixed> $target
437-
*/
438-
private function setFieldValue(array &$target, string $field, string|UploadedFile $value): void
439-
{
440-
$segments = $this->fieldSegments($field);
441-
442-
if ($segments === []) {
443-
return;
444-
}
445-
446-
$this->setNestedFieldValue($target, $segments, $value);
447-
}
448-
449-
/**
450-
* @return list<string>
451-
*/
452-
private function fieldSegments(string $field): array
453-
{
454-
if (! str_contains($field, '[')) {
455-
return [$field];
456-
}
457-
458-
$segments = [];
459-
$head = mb_strstr($field, '[', true);
460-
461-
if ($head !== false && $head !== '') {
462-
$segments[] = $head;
463-
}
464-
465-
preg_match_all('/\[([^\]]*)\]/', $field, $matches);
466-
467-
foreach ($matches[1] as $segment) {
468-
$segments[] = $segment;
469-
}
470-
471-
return $segments;
472-
}
473-
474-
/**
475-
* @param array<int|string, mixed> $target
476-
* @param list<string> $segments
477-
*/
478-
private function setNestedFieldValue(array &$target, array $segments, string|UploadedFile $value): void
479-
{
480-
$segment = array_shift($segments);
481-
482-
if ($segment === null) {
483-
return;
484-
}
485-
486-
if ($segments === []) {
487-
if ($segment === '') {
488-
$target[] = $value;
489-
490-
return;
491-
}
492-
493-
if (! array_key_exists($segment, $target)) {
494-
$target[$segment] = $value;
495-
496-
return;
497-
}
498-
499-
if (! is_array($target[$segment])) {
500-
$target[$segment] = [$target[$segment]];
501-
}
502-
503-
$target[$segment][] = $value;
504-
505-
return;
506-
}
507-
508-
if ($segment === '') {
509-
$target[] = [];
510-
511-
$lastKey = array_key_last($target);
512-
if (! is_array($target[$lastKey])) {
513-
return;
514-
}
515-
516-
$this->setNestedFieldValue($target[$lastKey], $segments, $value);
517-
518-
return;
519-
}
520-
521-
if (! isset($target[$segment]) || ! is_array($target[$segment])) {
522-
$target[$segment] = [];
523-
}
524-
525-
$this->setNestedFieldValue($target[$segment], $segments, $value);
526-
}
527-
528-
private function createUploadedFile(object $fileEntry): UploadedFile
529-
{
530-
$tempPath = tempnam(sys_get_temp_dir(), 'pest-browser-upload-');
531-
assert($tempPath !== false, 'Failed to create temporary upload file.');
532-
533-
$contents = method_exists($fileEntry, 'getContents') ? $fileEntry->getContents() : '';
534-
$contents = is_string($contents) ? $contents : '';
535-
536-
file_put_contents($tempPath, $contents);
537-
538-
$clientFilename = method_exists($fileEntry, 'getName') ? $fileEntry->getName() : 'upload';
539-
$clientFilename = is_string($clientFilename) && $clientFilename !== '' ? $clientFilename : 'upload';
540-
541-
$mimeType = method_exists($fileEntry, 'getMimeType') ? $fileEntry->getMimeType() : 'application/octet-stream';
542-
$mimeType = is_string($mimeType) && $mimeType !== '' ? $mimeType : 'application/octet-stream';
543-
544-
return new UploadedFile(
545-
$tempPath,
546-
$clientFilename,
547-
$mimeType,
548-
UPLOAD_ERR_OK,
549-
true,
550-
);
397+
return $this->extendedFormParser->parseMultipart($request);
551398
}
552399
}

0 commit comments

Comments
 (0)