Skip to content

Commit e3ae04c

Browse files
committed
#15: "Out of memory" sending large files
1 parent 24c6417 commit e3ae04c

File tree

4 files changed

+122
-3
lines changed

4 files changed

+122
-3
lines changed

src/Client.php

+21-3
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ public function sendRequest(RequestInterface $request)
123123
} catch (\Exception $e) {
124124
throw new RequestException($e->getMessage(), $request, $e);
125125
}
126+
126127
return $response;
127128
}
128129

@@ -161,6 +162,7 @@ public function sendAsyncRequest(RequestInterface $request)
161162
* @param RequestInterface $request
162163
*
163164
* @throws \UnexpectedValueException if unsupported HTTP version requested
165+
* @throws \RuntimeException if can not read body
164166
*
165167
* @return array
166168
*/
@@ -177,9 +179,23 @@ private function createCurlOptions(RequestInterface $request)
177179

178180
if (in_array($request->getMethod(), ['OPTIONS', 'POST', 'PUT'], true)) {
179181
// cURL allows request body only for these methods.
180-
$body = (string) $request->getBody();
181-
if ('' !== $body) {
182-
$options[CURLOPT_POSTFIELDS] = $body;
182+
$body = $request->getBody();
183+
$bodySize = $body->getSize();
184+
if ($bodySize !== 0) {
185+
// Message has non empty body.
186+
if (null === $bodySize || $bodySize > 1024 * 1024) {
187+
// Avoid full loading large or unknown size body into memory
188+
$options[CURLOPT_UPLOAD] = true;
189+
if (null !== $bodySize) {
190+
$options[CURLOPT_INFILESIZE] = $bodySize;
191+
}
192+
$options[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) {
193+
return $body->read($length);
194+
};
195+
} else {
196+
// Small body can be loaded into memory
197+
$options[CURLOPT_POSTFIELDS] = (string) $body;
198+
}
183199
}
184200
}
185201

@@ -221,6 +237,7 @@ private function getProtocolVersion($requestVersion)
221237
}
222238
throw new \UnexpectedValueException('libcurl 7.33 needed for HTTP 2.0 support');
223239
}
240+
224241
return CURL_HTTP_VERSION_NONE;
225242
}
226243

@@ -249,6 +266,7 @@ private function createHeaders(RequestInterface $request, array $options)
249266
$curlHeaders[] = $name . ': ' . $value;
250267
}
251268
}
269+
252270
return $curlHeaders;
253271
}
254272
}

tests/HttpClientDiactorosTest.php

+14
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
use Http\Client\HttpClient;
66
use Http\Message\MessageFactory\DiactorosMessageFactory;
77
use Http\Message\StreamFactory\DiactorosStreamFactory;
8+
use Psr\Http\Message\StreamInterface;
89
use Zend\Diactoros\Request;
910
use Zend\Diactoros\Response;
11+
use Zend\Diactoros\Stream;
1012

1113
/**
1214
* Tests for Http\Client\Curl\Client
@@ -20,4 +22,16 @@ protected function createHttpAdapter()
2022
{
2123
return new Client(new DiactorosMessageFactory(), new DiactorosStreamFactory());
2224
}
25+
26+
/**
27+
* Create stream from file
28+
*
29+
* @param string $filename
30+
*
31+
* @return StreamInterface
32+
*/
33+
protected function createFileStream($filename)
34+
{
35+
return new Stream($filename);
36+
}
2337
}

tests/HttpClientGuzzleTest.php

+14
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
<?php
22
namespace Http\Client\Curl\Tests;
33

4+
use GuzzleHttp\Psr7\Stream;
45
use Http\Client\Curl\Client;
56
use Http\Client\HttpClient;
67
use Http\Message\MessageFactory\GuzzleMessageFactory;
78
use Http\Message\StreamFactory\GuzzleStreamFactory;
9+
use Psr\Http\Message\StreamInterface;
810

911
/**
1012
* Tests for Http\Client\Curl\Client
@@ -18,4 +20,16 @@ protected function createHttpAdapter()
1820
{
1921
return new Client(new GuzzleMessageFactory(), new GuzzleStreamFactory());
2022
}
23+
24+
/**
25+
* Create stream from file
26+
*
27+
* @param string $filename
28+
*
29+
* @return StreamInterface
30+
*/
31+
protected function createFileStream($filename)
32+
{
33+
return new Stream(fopen($filename, 'r'));
34+
}
2135
}

tests/HttpClientTestCase.php

+73
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,21 @@
22
namespace Http\Client\Curl\Tests;
33

44
use Http\Client\Tests\HttpClientTest;
5+
use Http\Client\Tests\PHPUnitUtility;
6+
use Psr\Http\Message\StreamInterface;
57

68
/**
79
* Base class for client integration tests
810
*/
911
abstract class HttpClientTestCase extends HttpClientTest
1012
{
13+
/**
14+
* Temporary file name created by test.
15+
*
16+
* @var string[]
17+
*/
18+
protected $tmpFiles = [];
19+
1120
/**
1221
* @dataProvider requestProvider
1322
* @group integration
@@ -51,4 +60,68 @@ public function testSendRequestWithOutcome(
5160
$body
5261
);
5362
}
63+
64+
/**
65+
* Test sending large files.
66+
*/
67+
public function testSendLargeFile()
68+
{
69+
$filename = $this->createTempFile();
70+
$fd = fopen($filename, 'a');
71+
$buffer = str_repeat('x', 1024);
72+
for ($i = 0; $i < 2048; $i++) {
73+
fwrite($fd, $buffer);
74+
}
75+
fclose($fd);
76+
$body = $this->createFileStream($filename);
77+
78+
$request = self::$messageFactory->createRequest(
79+
'POST',
80+
PHPUnitUtility::getUri(),
81+
[],
82+
$body
83+
);
84+
85+
$response = $this->httpAdapter->sendRequest($request);
86+
$this->assertResponse(
87+
$response,
88+
[
89+
'body' => 'Ok',
90+
]
91+
);
92+
}
93+
94+
/**
95+
* Create temp file.
96+
*
97+
* @return string Filename
98+
*/
99+
protected function createTempFile()
100+
{
101+
$filename = tempnam(sys_get_temp_dir(), 'tests');
102+
$this->tmpFiles[] = $filename;
103+
104+
return $filename;
105+
}
106+
107+
/**
108+
* Create stream from file
109+
*
110+
* @param string $filename
111+
*
112+
* @return StreamInterface
113+
*/
114+
abstract protected function createFileStream($filename);
115+
116+
/**
117+
* Tears down the fixture
118+
*/
119+
protected function tearDown()
120+
{
121+
parent::tearDown();
122+
123+
foreach ($this->tmpFiles as $filename) {
124+
@unlink($filename);
125+
}
126+
}
54127
}

0 commit comments

Comments
 (0)