Environment: Guzzle 6 Symfony 2.3
Uploading multiple files via a Guzzle POST request should be done with a multipart request. So I configure my $options array like so:
Array
(
[multipart] => Array
(
[0] => Array
(
[name] => filename-0
[contents] => Resource id #440
[filename] => filename-0
)
[1] => Array
(
[name] => filename-1
[contents] => Resource id #441
[filename] => filename-1
)
[2] => Array
(
[name] => filename-2
[contents] => Resource id #442
[filename] => filename-2
)
)
[headers] => Array
(
[Accept] => application/json
[Content-Type] => multipart/form-data
[Accept-Language] => de
)
[allow_redirects] =>
[http_errors] =>
)
The resources in the multipart-array are the results of an fopen().
And send the request using
$response = $this->client->post(
'/someUrl/someAction',
$options
);
Using an already created client.
On the accepting Symfony-Controllers side, I can't get the files sent:
var_dump($_FILES); // array(0) {}
var_dump($_POST); // array(0) {}
var_dump(count($request->files->all())); // int(0)
However, both of these:
var_dump(file_get_contents("php://input"));
var_dump($request->getContent());
Return data on the input stream:
/myPath/FileController.php:xx:
string(601) "--e55f849feb078da4a9e35ba77da3ded02ec813a7
Content-Disposition: form-data; name="filename-0"; filename="filename-0"
Content-Length: 43
This is a testfile...
--e55f849feb078da4a9e35ba77da3ded02ec813a7
Content-Disposition: form-data; name="filename-1"; filename="filename-1"
Content-Length: 43
This is a testfile...
--e55f849feb078da4a9e35ba77da3ded02ec813a7
Content-Disposition: form-data; name="filename-2"; filename="filename-2"
Content-Length: 43
This is a testfile...
--e55f849feb078da4a9e35ba77da3ded02ec813a7--
"
How might I get in the receiving controller in a Symfony-way?
A curiosity to consider: The Controller reports
var_dump($request->getContentType()); // NULL
My gut feeling says this might be important.
When using the multipart
option, you must not specify the Content-Type
header yourself. Guzzle will take care of this and - more important for this issue - of the boundary that separates the content parts in the raw request body. The main Content-Type
header defines that boundary like so:
Content-Type: multipart/form-data; boundary=unique-string-that-is-hopefully-not-used-in-the-real-content-because-it-separates-the-content-parts`
Check Client.php#L308:
Guzzle creates the request body as GuzzleHttp\Psr7\MultipartStream
object when multipart
option was used.
Then check the MultipartStream
constructor at MultipartStream.php#L30:
A random value is created to be used as the boundary: sha1(uniqid('', true))
Guzzle will then use the boundary to set the correct content type on its own at Client.php#L383.
But as Guzzle does not override an option that you have specified explicitly, the main header specifies an empty boundary and the raw body parts will be separated by the one created by the MultpartStream
constructor. On the receiving end PHP cannot separate the content parts any more.
This code works for me:
(new GuzzleHttp\Client())->request(
'POST',
'http://localhost/upload',
[
'multipart' => [
[
'name' => 'filename-0',
'contents' => fopen(__DIR__.'/sample-0.txt', 'rb'),
'filename' => 'filename-0',
],
[
'name' => 'filename-1',
'contents' => fopen(__DIR__.'/sample-1.txt', 'rb'),
'filename' => 'filename-1',
],
[
'name' => 'filename-2',
'contents' => fopen(__DIR__.'/sample-2.txt', 'rb'),
'filename' => 'filename-2',
],
],
'headers' => [
# Do not override the Content-Type header here, Guzzle takes care of it
] ,
]
);
Please note that all of this is not Symfony related, i.e. Symfony just creates it Request
object from what it can get via PHP's native sources like $_FILES
, $_POST
etc. You were able to see all the content in the raw request body ($request->getContent()
) because it was sent like this, but PHP could not split the content parts because it didn't knew of the correct boundary.