Skip to content

Commit bea0356

Browse files
committed
Failed upload will throw the new UploadError #11708
Previously it was up to the application to validate that the received `UploadedFileInterface` was `$file->getError() === UPLOAD_ERR_OK`. This is now included in `UploadType` and all `UploadedFileInterface` received by the application are guaranteed to be successful uploads.
1 parent ba295cf commit bea0356

File tree

5 files changed

+83
-3
lines changed

5 files changed

+83
-3
lines changed

src/UploadError.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace GraphQL\Upload;
6+
7+
use Exception;
8+
use GraphQL\Error\Error;
9+
10+
final class UploadError extends Error
11+
{
12+
public function __construct(int $uploadError)
13+
{
14+
parent::__construct('File upload: ' . $this->getMessageFromUploadError($uploadError));
15+
}
16+
17+
private function getMessageFromUploadError(int $uploadError): string
18+
{
19+
return match ($uploadError) {
20+
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
21+
UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the upload',
22+
UPLOAD_ERR_FORM_SIZE => 'The file exceeds the `MAX_FILE_SIZE` directive that was specified in the HTML form',
23+
UPLOAD_ERR_INI_SIZE => 'The file exceeds the `upload_max_filesize` of ' . Utility::toMebibyte(Utility::getUploadMaxFilesize()),
24+
UPLOAD_ERR_NO_FILE => 'No file was uploaded',
25+
UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder',
26+
UPLOAD_ERR_PARTIAL => 'The file was only partially uploaded',
27+
default => throw new Exception('Unsupported UPLOAD_ERR_* constant value: ' . $uploadError),
28+
};
29+
}
30+
}

src/UploadType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ public function parseValue(mixed $value): UploadedFileInterface
3737
throw new UnexpectedValueException('Could not get uploaded file, be sure to conform to GraphQL multipart request specification. Instead got: ' . Utils::printSafe($value));
3838
}
3939

40+
$error = $value->getError();
41+
if ($error !== UPLOAD_ERR_OK) {
42+
throw new UploadError($error);
43+
}
44+
4045
return $value;
4146
}
4247

src/Utility.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,17 @@ final class Utility
1111
{
1212
public static function getPostMaxSize(): int
1313
{
14-
return ini_parse_quantity(ini_get('post_max_size') ?: '0');
14+
return self::fromIni('post_max_size');
15+
}
16+
17+
public static function getUploadMaxFilesize(): int
18+
{
19+
return self::fromIni('upload_max_filesize');
20+
}
21+
22+
private static function fromIni(string $key): int
23+
{
24+
return ini_parse_quantity(ini_get($key) ?: '0');
1525
}
1626

1727
/**

tests/Psr7/PsrUploadedFileStub.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
final class PsrUploadedFileStub extends UploadedFile
1010
{
11-
public function __construct(string $clientFilename, string $clientMediaType)
11+
public function __construct(string $clientFilename, string $clientMediaType, int $errorStatus = UPLOAD_ERR_OK)
1212
{
13-
parent::__construct('foo', 123, UPLOAD_ERR_OK, $clientFilename, $clientMediaType);
13+
parent::__construct('foo', 123, $errorStatus, $clientFilename, $clientMediaType);
1414
}
1515
}

tests/UploadTypeTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44

55
namespace GraphQLTests\Upload;
66

7+
use Exception;
78
use GraphQL\Error\Error;
89
use GraphQL\Error\InvariantViolation;
910
use GraphQL\Language\AST\StringValueNode;
11+
use GraphQL\Upload\UploadError;
1012
use GraphQL\Upload\UploadType;
13+
use GraphQL\Upload\Utility;
1114
use GraphQLTests\Upload\Psr7\PsrUploadedFileStub;
1215
use PHPUnit\Framework\TestCase;
16+
use Throwable;
1317
use UnexpectedValueException;
1418

1519
final class UploadTypeTest extends TestCase
@@ -49,4 +53,35 @@ public function testCanNeverParseLiteral(): void
4953
$this->expectExceptionMessage('`Upload` cannot be hardcoded in query, be sure to conform to GraphQL multipart request specification. Instead got: StringValue');
5054
$type->parseLiteral($node);
5155
}
56+
57+
/**
58+
* @param class-string<Throwable> $e
59+
*
60+
* @dataProvider providerUploadErrorWillThrow
61+
*/
62+
public function testUploadErrorWillThrow(int $errorStatus, string $expectedMessage, string $e = UploadError::class): void
63+
{
64+
$type = new UploadType();
65+
$file = new PsrUploadedFileStub('image.jpg', 'image/jpeg', $errorStatus);
66+
67+
$this->expectException($e);
68+
$this->expectExceptionMessage($expectedMessage);
69+
70+
$type->parseValue($file);
71+
}
72+
73+
/**
74+
* @return iterable<array{0: int, 1: string, 2?: class-string<Throwable>}>
75+
*/
76+
public static function providerUploadErrorWillThrow(): iterable
77+
{
78+
yield [UPLOAD_ERR_CANT_WRITE, 'Failed to write file to disk'];
79+
yield [UPLOAD_ERR_EXTENSION, 'A PHP extension stopped the upload'];
80+
yield [UPLOAD_ERR_FORM_SIZE, 'The file exceeds the `MAX_FILE_SIZE` directive that was specified in the HTML form'];
81+
yield [UPLOAD_ERR_INI_SIZE, 'The file exceeds the `upload_max_filesize` of ' . Utility::toMebibyte(Utility::getUploadMaxFilesize())];
82+
yield [UPLOAD_ERR_NO_FILE, 'No file was uploaded'];
83+
yield [UPLOAD_ERR_NO_TMP_DIR, 'Missing a temporary folder'];
84+
yield [UPLOAD_ERR_PARTIAL, 'The file was only partially uploaded'];
85+
yield [5, 'Unsupported UPLOAD_ERR_* constant value: 5', Exception::class];
86+
}
5287
}

0 commit comments

Comments
 (0)