Testing File Uploads

I search the few posts that were related on this forum but they appeared to have diverged down a different path. Here are my related methods for attempting to create a POST request that uploads a file that is local.


    final public function postUpload(string $uri, string $file, string $mime, string $name = 'file', array $data = [], array $headers = []): TestResponse
    {
        $request = $this->createFormRequest(RequestMethodInterface::METHOD_POST, $uri, $data);
        $request = $request->withHeader('Content-Type', 'multipart/form-data');

        $uploadFile = new UploadedFile($file, basename($file), $mime, filesize($file));
        $request    = $request->withUploadedFiles([$name => [$uploadFile]]);

        return $this->send($request, $headers);
    }

    private function send(MessageInterface|ServerRequestInterface $request, array $headers): TestResponse
    {
        if (property_exists(static::class, 'defaultHeaders')) {
            $headers = array_merge($this->defaultHeaders, $headers);
        }

        foreach ($headers as $name => $value) {
            $request = $request->withHeader($name, $value);
        }

        return TestResponse::fromBaseResponse($this->app->handle($request));
    }

The request does indeed get made. However, the file does not seem to get added to the request. If I analyze the $request object before handling the request, the file upload information is 100% there.

This will ultimately be a PR that gets submitted into this phpunit plugin: GitHub - nekofar/slim-test: Slim Framework test helper built on top of the PHPUnit test framework

The withUploadedFiles method accepts an array of “UploadedFile”. The index should be numeric value (and not a string). So maybe try to something like:

$request = $request->withUploadedFiles([$uploadFile]);

For testing purpose, I would also recommend using an in-memory stream in combination with vfsStream.

I had tried that before with withUploadedFiles and it’s still was not working the files are never sent with the request. I retested and that is still the case. The request happens but $_FILES is empty.

Using a local file path may use more memory but it should still work. I figure that I could try to switch to using a Stream once I can get the file to upload at all.

This is not a bug, but correct and the purpose of the test because when you run tests, you simulate the request without a real webserver environment. When you use the PSR-7 request object only (and no $_FILES in your code) everything should work.

This makes sense. I was only doing a var_dump on $_FILES to see if it had contents in the request. I am using UploadManager. It supports file chunking. have you used this before? Do you have a better recommendation?

$uploadManager = new UploadManager($args['property']);

$uploadManager->afterUpload(function ($chunk) use ($args, &$object) {
	$filepath = $chunk->getSavePath() . $chunk->getNameWithExtension();
	if ($chunk->hasError() && file_exists($filepath)) {
		// remove current chunk on error
		return unlink($filepath);
	}
});
$uploadManager->upload($this->config->tmpDir);

So I do see the file data under $request->getUploadedFiles() so it looks like I need to redo my upload code. Any clues on how to support file chunking with a PSR-7 request?

You need to manage individual chunks, assemble them in the correct order, and handle potential issues like missing or duplicated chunks.

Your chunked upload handler may also depend on the HTTP protocol, whether you use HTTP 1.1 or HTTP/2. Because chunked transfer encoding is not supported in HTTP/2, which provides its own mechanisms for data streaming.

The RFC 9112 (formerly known as RFC 7230) section 7.1 specifies the use of the Transfer-Encoding: chunked header in HTTP/1.1 for chunked transfer coding. This is a way to send a resource in chunks, possibly before knowing the total size of the resource.

Below is a basic example of how you could implement the handling of HTTP/1.1 requests with “chunked” Transfer-Encoding.

$app->post('/upload-chunked', function (ServerRequestInterface $request, ResponseInterface $response) {
    $transferEncoding = $request->getHeaderLine('Transfer-Encoding');
    $filename = $request->getHeaderLine('X-File-Name');

    if (empty($filename)) {
        $response = $response->withStatus(400);
        $response->getBody()->write('Bad Request: Filename is required');
        return $response;
    }

    if ($transferEncoding !== 'chunked') {
        $response = $response->withStatus(400);
        $response->getBody()->write('Bad Request: Expected chunked Transfer-Encoding');
        return $response;
    }

    // Prepare the temp file to which the chunks will be written
    $tempPath = __DIR__ . '/temp/' . $filename . '.part';
    $tempFile = fopen($tempPath, 'ab');

    $input = $request->getBody();
    while (!$input->eof()) {
        $chunkHeader = trim($input->readLine());
        $chunkSize = hexdec($chunkHeader);
        if ($chunkSize === 0) {
            break;
        }

        $chunkData = $input->read($chunkSize);
        fwrite($tempFile, $chunkData);

        $input->read(2);  // Skip the trailing CRLF after the chunk
    }

    fclose($tempFile);

    $response->getBody()->write("Successfully received chunked data, stored in {$tempPath}.");
    return $response;
});

To test this out, you can use curl like so:

curl -X POST -H "Transfer-Encoding: chunked" -H "X-File-Name: my-file.txt" --data-binary @your-file-to-upload http://localhost:8080/upload-chunked

Note that this is a simplified example and does not cover many aspects you would want to handle in a real-world application, such as error handling, validations, etc.