Sending multipart data with PSR-18
Tags: php, psr, http, multipart
Published: 2019-10-24 07:02:58

While [migrating](https://medium.com/@timoschinkel/implementing-psr-18-and-extending-it-with-middleware-b33eeceb2753) our large codebase from Guzzle - and a little bit of cURL here and there - to [PSR-18](https://www.php-fig.org/psr/psr-18/) I came across the following snippet:

```php
$file = new \GuzzleHttp\Post\PostFile('file', $screenshotData, 'screenshot.png');

$this->httpClient->post(
    "https://...",
    ['body' => 
        'name' => $name,
        'file' => $file
    ]
);
```

This snippet creates a POST request as `multipart/form-data` with a parameter `file` that contains the data of the file that is uploaded. This effectively emulates a simple HTML form:

```html
<form method="post" action="https://..." enctype="multipart/form-data">
    <input type="text" name="name" />
    <input type="file" name="file" />
</form>
```

Although we implement PSR-18 using Guzzle - via HttPlug - I want to refrain from using Guzzle specific classes in our codebase, since that might prevent us from having a smooth migration to a different HTTP abstraction. And it is not compliant with PSR-18. 

## Sending a post request
There are two methods of sending data with a POST request; `application/x-www-form-urlencoded` or `multipart/form-data`. The first is a translation of the GET structure to a POST body:

```
POST /a/post/url HTTP/1.1
Host: www.domain.ext
Content-Type: application/x-www-form-urlencoded

field=value&another+field=another+value
```

This body is achievable via PHP's [`http_build_query`](https://www.php.net/manual/en/function.http-build-query.php): 

```php
$body = http_build_query(
    ['field' => 'value', 'another field' => 'another value'], 
    '', 
    '&', 
    PHP_QUERY_RFC1738
);
```

But using this method of creating a POST request does not allow for the submission of attachments. For those situations you'll need to resort to `multipart/form-data`.

## multipart/form-data
An attachment is not just the (binary) data of a file, it is a filename, a content type and possible other meta information. As this needs some form of structure the `multipart/form-data` content type was introduced in [`RFC 2388`](https://tools.ietf.org/html/rfc2388).
 
Effectively what the code above does is create a request that looks like this:

```
POST /a/post/url HTTP/1.1
Host: www.domain.ext
Content-Type: multipart/format-data; boundary="a.random.boundary"

--a.random.boundary
Content-Disposition: form-data; name="name"
Content-Length: 4

name
--a.random.boundary
Content-Disposition: form-data; name="file"; filename="screenshot.png"
Content-Type: image.png

{$screenshotData}
--a.random.boundary--
```

And although this is doable using the PSR-18 and PSR-15 interfaces, it requires some knowledge about how this part of the HTTP spec works:

```php
$boundary = 'a.random.boundary';
$request = (new RequestFactory())
    ->createRequest('POST', 'https://www.domain.ext/a/post/url')
    ->withHeader('Content-Type', "multipart/form-data; boundary=\"{$boundary}\"")
    ->withBody((new StreamFactory())->createStream("--{$boundary}" . "\r\n".
"Content-Disposition: form-data; name=\"name\"" . "\r\n" .
"\r\n" . 
"{$name}" . "\r\n" . 
"--{$boundary}" . "\r\n" .    
"Content-Disposition: form-data; name=\"file\"; filename=\"screenshot.php\"" . "\r\n" .
"Content-Type: image/png" . "\r\n" .
"\r\n" . 
"{$screenShotData}" . "\r\n" .
"--{$boundary}--"));

$this->httpClient->sendRequest($request);
```

This is a lot of work and it has a few pitfalls; while trying this I used a PHP server as receiving party and the requests were handled properly. But when I tried to send a file to JIRA I got an error back saying `Header section has more than 10240 bytes (maybe it is not properly terminated)`. After a fair amount of debugging I found that JIRA (or Java) requires `\r\n` as new line character. 

Now I can create a nice library for, but I am a bit lazy, so I typically turn to Packagist first to see if someone has done this already. When searching Packagist for packages to handle all of this for me you'll find a number of packages, but ons stands out downloads and stars wise: [`php-http/multipart-stream-builder`](https://packagist.org/packages/php-http/multipart-stream-builder). 

## Multipart stream builder
The Multipart stream builder is a package authored by the team that is also responsible for HttPlug. It is - as the name suggests - actually meant to construct multipart streams:

> A builder for Multipart PSR-7 Streams. The builder create streams independently form any PSR-7 implementation.

In order to create a PSR-7 compliant request with a multipart body we need to create an instance of the multipart stream builder:

```php
use Http\Message\MultipartStream\MultipartStreamBuilder;

$builder = new MultipartStreamBuilder($streamFactory);
$builder->addResource(
    'file', 
    fopen('/path/to/uploaded/file', 'r'), 
    [
        'filename' => 'filename.ext', 
        'headers' => ['Content-Type' => 'application/octet-stream']
    ]
);

$request = $requestFactory
    ->createRequest('POST', 'https://...')
    ->withHeader('Content-Type', 'multipart/form-data; boundary="' . $builder->getBoundary() . '"')
    ->withBody($builder->build());
    
$response = $client->sendRequest($request);    
```

The nice thing about this library is that it returns an instance of `\Psr\Http\Message\StreamInterface`. Incidentally the same type of object `\Psr\Http\Message\MessageInterface::withBody()` expects.

A downside of this library is that the stream builder is not immutable. Injecting it into your application can be a bit tricky as your DI container should return a new instance of the stream builder every time it is injected. Next to that I like to inject based on an interface, which is a personal preference. A small workaround for this is easily created via a factory:

```php
<?php
declare(strict_types=1);

use Http\Message\MultipartStream\MultipartStreamBuilder;
use Psr\Http\Message\StreamFactoryInterface;

final class MultipartStreamBuilderFactory implements MultipartStreamBuilderFactoryInterface
{
    /** @var StreamFactoryInterface */
    private $streamFactory;

    public function __construct(StreamFactoryInterface $streamFactory)
    {
        $this->streamFactory = $streamFactory;
    }

    public function build(): MultipartStreamBuilder
    {
        return new MultipartStreamBuilder($this->streamFactory);
    }
}

```

Now you don't have to worry about the factory being shared between different resources; you can always get a new instance of the stream builder.

## Symfony Mime
Although it did not show up on the Packagist search Symfony has a component to do this for you as well. The primary goal of this package is aimed to be used to create MIME messages and is primarily focused on email messages - MIME is an acronym for Multipurpose Internet Mail Extensions. But it also has a feature - currently marked as experimental - that can be used to create multipart messages; `FormDataPart`. This feature is described in the documentation of the [Symfony Http Client](https://symfony.com/doc/current/components/http_client.html#uploading-data):

> To submit a form with file uploads, it is your responsibility to encode the body according to the multipart/form-data content-type. The Symfony Mime component makes it a few lines of code:
> 
> [...]


This statement is partly true; if you're using the Symfony Http Client only a few lines of code are needed. But if you're using a PSR-18 compliant client a few more lines are needed:

```php
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\Multipart\FormDataPart;

$formFields = [
    'regular_field' => 'some value',
    'file_field' => DataPart::fromPath('/path/to/uploaded/file'),
];

$request = $requestFactory
    ->createRequest('POST', 'https://...');

$formData = new FormDataPart($formFields);
$preparedHeaders = $formData->getPreparedHeaders();
foreach ($preparedHeaders->getNames() as $header) {
    $request = $request->withHeader(
        $header, 
        $preparedHeaders->get($header)->getBodyAsString()
    );
}

$request = $request->withBody(
    $streamFactory->createStream($formData->bodyToString())
);

$response = $client->sendRequest($request);
```

## Conclusion
You can build up a multipart message yourself, but you might run into unexpected issues - like new line character incompatibilities. Chances are you are not the first that is facing such a situation and chances are that it is already a solved issue. An example of this is `php-http/multipart-stream-builder`.

The two major HTTP client abstraction libraries - Guzzle and Symfony HttpClient - have a lot built-in functionalities that PSR-18 does not offer. This makes that when using PSR-18 you might need to have more knowledge of how HTTP actually works. I personally don't think this is a bad thing. Here lie new opportunities as we can now use clients that only perform requests and move all additional functionalities to separate packages that have a single responsibility and you only need to add to your codebase if you actually use them.