Skip to content

Commit 5f7e835

Browse files
committed
#20: Minimize memory usage when reading large response body
1 parent c5b5c80 commit 5f7e835

9 files changed

+141
-161
lines changed

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
},
2121
"require-dev": {
2222
"guzzlehttp/psr7": "^1.0",
23-
"php-http/adapter-integration-tests": "dev-master#836cdff8294174cceeae54601ab4079c309227b7",
24-
"php-http/message": "^1.0.2",
23+
"php-http/adapter-integration-tests": "0.4.*",
24+
"php-http/message": "^1.2",
2525
"php-http/discovery": "~0.8.0",
2626
"phpunit/phpunit": "^4.8",
2727
"puli/composer-plugin": "1.0.0-beta9",

puli.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
"installer": "composer",
3838
"env": "dev"
3939
},
40+
"paragonie/random_compat": {
41+
"install-path": "vendor/paragonie/random_compat",
42+
"installer": "composer",
43+
"env": "dev"
44+
},
4045
"php-http/adapter-integration-tests": {
4146
"install-path": "vendor/php-http/adapter-integration-tests",
4247
"installer": "composer",

src/Client.php

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,18 @@ class Client implements HttpClient, HttpAsyncClient
3232
private $options;
3333

3434
/**
35-
* cURL response parser
35+
* PSR-7 message factory
3636
*
37-
* @var ResponseParser
37+
* @var MessageFactory
3838
*/
39-
private $responseParser;
39+
private $messageFactory;
40+
41+
/**
42+
* PSR-7 stream factory
43+
*
44+
* @var StreamFactory
45+
*/
46+
private $streamFactory;
4047

4148
/**
4249
* cURL synchronous requests handle
@@ -66,7 +73,8 @@ public function __construct(
6673
StreamFactory $streamFactory,
6774
array $options = []
6875
) {
69-
$this->responseParser = new ResponseParser($messageFactory, $streamFactory);
76+
$this->messageFactory = $messageFactory;
77+
$this->streamFactory = $streamFactory;
7078
$this->options = $options;
7179
}
7280

@@ -87,15 +95,16 @@ public function __destruct()
8795
*
8896
* @return ResponseInterface
8997
*
90-
* @throws RequestException
98+
* @throws \RuntimeException If creating the body stream fails.
9199
* @throws \UnexpectedValueException if unsupported HTTP version requested
92-
* @throws \RuntimeException if can not read body
100+
* @throws RequestException
93101
*
94102
* @since 1.0
95103
*/
96104
public function sendRequest(RequestInterface $request)
97105
{
98-
$options = $this->createCurlOptions($request);
106+
$responseBuilder = $this->createResponseBuilder();
107+
$options = $this->createCurlOptions($request, $responseBuilder);
99108

100109
if (is_resource($this->handle)) {
101110
curl_reset($this->handle);
@@ -104,19 +113,14 @@ public function sendRequest(RequestInterface $request)
104113
}
105114

106115
curl_setopt_array($this->handle, $options);
107-
$raw = curl_exec($this->handle);
116+
curl_exec($this->handle);
108117

109118
if (curl_errno($this->handle) > 0) {
110119
throw new RequestException(curl_error($this->handle), $request);
111120
}
112121

113-
$info = curl_getinfo($this->handle);
114-
115-
try {
116-
$response = $this->responseParser->parse($raw, $info);
117-
} catch (\Exception $e) {
118-
throw new RequestException($e->getMessage(), $request, $e);
119-
}
122+
$response = $responseBuilder->getResponse();
123+
$response->getBody()->seek(0);
120124

121125
return $response;
122126
}
@@ -128,23 +132,24 @@ public function sendRequest(RequestInterface $request)
128132
*
129133
* @return Promise
130134
*
135+
* @throws \RuntimeException If creating the body stream fails.
136+
* @throws \UnexpectedValueException If unsupported HTTP version requested
131137
* @throws Exception
132-
* @throws \UnexpectedValueException if unsupported HTTP version requested
133-
* @throws \RuntimeException if can not read body
134138
*
135139
* @since 1.0
136140
*/
137141
public function sendAsyncRequest(RequestInterface $request)
138142
{
139143
if (!$this->multiRunner instanceof MultiRunner) {
140-
$this->multiRunner = new MultiRunner($this->responseParser);
144+
$this->multiRunner = new MultiRunner();
141145
}
142146

143147
$handle = curl_init();
144-
$options = $this->createCurlOptions($request);
148+
$responseBuilder = $this->createResponseBuilder();
149+
$options = $this->createCurlOptions($request, $responseBuilder);
145150
curl_setopt_array($handle, $options);
146151

147-
$core = new PromiseCore($request, $handle);
152+
$core = new PromiseCore($request, $handle, $responseBuilder);
148153
$promise = new CurlPromise($core, $this->multiRunner);
149154
$this->multiRunner->add($core);
150155

@@ -155,18 +160,19 @@ public function sendAsyncRequest(RequestInterface $request)
155160
* Generates cURL options
156161
*
157162
* @param RequestInterface $request
163+
* @param ResponseBuilder $responseBuilder
158164
*
159165
* @throws \UnexpectedValueException if unsupported HTTP version requested
160166
* @throws \RuntimeException if can not read body
161167
*
162168
* @return array
163169
*/
164-
private function createCurlOptions(RequestInterface $request)
170+
private function createCurlOptions(RequestInterface $request, ResponseBuilder $responseBuilder)
165171
{
166172
$options = $this->options;
167173

168-
$options[CURLOPT_HEADER] = true;
169-
$options[CURLOPT_RETURNTRANSFER] = true;
174+
$options[CURLOPT_HEADER] = false;
175+
$options[CURLOPT_RETURNTRANSFER] = false;
170176
$options[CURLOPT_FOLLOWLOCATION] = false;
171177

172178
$options[CURLOPT_HTTP_VERSION] = $this->getProtocolVersion($request->getProtocolVersion());
@@ -207,6 +213,23 @@ private function createCurlOptions(RequestInterface $request)
207213
$options[CURLOPT_USERPWD] = $request->getUri()->getUserInfo();
208214
}
209215

216+
$options[CURLOPT_HEADERFUNCTION] = function ($ch, $data) use ($responseBuilder) {
217+
$str = trim($data);
218+
if ('' !== $str) {
219+
if (strpos(strtolower($str), 'http/') === 0) {
220+
$responseBuilder->setStatus($str)->getResponse();
221+
} else {
222+
$responseBuilder->addHeader($str);
223+
}
224+
}
225+
226+
return strlen($data);
227+
};
228+
229+
$options[CURLOPT_WRITEFUNCTION] = function ($ch, $data) use ($responseBuilder) {
230+
return $responseBuilder->getResponse()->getBody()->write($data);
231+
};
232+
210233
return $options;
211234
}
212235

@@ -274,4 +297,23 @@ private function createHeaders(RequestInterface $request, array $options)
274297

275298
return $curlHeaders;
276299
}
300+
301+
/**
302+
* Create new ResponseBuilder instance
303+
*
304+
* @return ResponseBuilder
305+
*
306+
* @throws \RuntimeException If creating the stream from $body fails.
307+
*/
308+
private function createResponseBuilder()
309+
{
310+
try {
311+
$body = $this->streamFactory->createStream(fopen('php://temp', 'w+'));
312+
} catch (\InvalidArgumentException $e) {
313+
throw new \RuntimeException('Can not create "php://temp" stream.');
314+
}
315+
$response = $this->messageFactory->createResponse(200, null, [], $body);
316+
317+
return new ResponseBuilder($response);
318+
}
277319
}

src/CurlPromise.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public function wait($unwrap = true)
9898
$this->runner->wait($this->core);
9999

100100
if ($unwrap) {
101-
if ($this->core->getState() == self::REJECTED) {
101+
if ($this->core->getState() === self::REJECTED) {
102102
throw $this->core->getException();
103103
}
104104

src/MultiRunner.php

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,13 @@ class MultiRunner
1919
*/
2020
private $multiHandle = null;
2121

22-
/**
23-
* cURL response parser
24-
*
25-
* @var ResponseParser
26-
*/
27-
private $responseParser;
28-
2922
/**
3023
* Awaiting cores
3124
*
3225
* @var PromiseCore[]
3326
*/
3427
private $cores = [];
3528

36-
/**
37-
* Construct new runner.
38-
*
39-
* @param ResponseParser $responseParser
40-
*/
41-
public function __construct(ResponseParser $responseParser)
42-
{
43-
$this->responseParser = $responseParser;
44-
}
45-
4629
/**
4730
* Release resources if still active
4831
*/
@@ -111,11 +94,7 @@ public function wait(PromiseCore $targetCore = null)
11194

11295
if (CURLE_OK === $info['result']) {
11396
try {
114-
$response = $this->responseParser->parse(
115-
curl_multi_getcontent($core->getHandle()),
116-
curl_getinfo($core->getHandle())
117-
);
118-
$core->fulfill($response);
97+
$core->fulfill();
11998
} catch (\Exception $e) {
12099
$core->reject(
121100
new RequestException($e->getMessage(), $core->getRequest(), $e)

src/PromiseCore.php

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ class PromiseCore
2929
*/
3030
private $handle;
3131

32+
/**
33+
* Response builder
34+
*
35+
* @var ResponseBuilder
36+
*/
37+
private $responseBuilder;
38+
3239
/**
3340
* Promise state
3441
*
@@ -57,26 +64,24 @@ class PromiseCore
5764
*/
5865
private $onRejected = [];
5966

60-
/**
61-
* Received response
62-
*
63-
* @var ResponseInterface|null
64-
*/
65-
private $response = null;
66-
6767
/**
6868
* Create shared core.
6969
*
7070
* @param RequestInterface $request HTTP request
7171
* @param resource $handle cURL handle
72+
* @param ResponseBuilder $responseBuilder
7273
*/
73-
public function __construct(RequestInterface $request, $handle)
74-
{
74+
public function __construct(
75+
RequestInterface $request,
76+
$handle,
77+
ResponseBuilder $responseBuilder
78+
) {
7579
assert('is_resource($handle)');
7680
assert('get_resource_type($handle) === "curl"');
7781

7882
$this->request = $request;
7983
$this->handle = $handle;
84+
$this->responseBuilder = $responseBuilder;
8085
$this->state = Promise::PENDING;
8186
}
8287

@@ -90,7 +95,10 @@ public function addOnFulfilled(callable $callback)
9095
if ($this->getState() === Promise::PENDING) {
9196
$this->onFulfilled[] = $callback;
9297
} elseif ($this->getState() === Promise::FULFILLED) {
93-
$this->response = call_user_func($callback, $this->response);
98+
$response = call_user_func($callback, $this->responseBuilder->getResponse());
99+
if ($response instanceof ResponseInterface) {
100+
$this->responseBuilder->setResponse($response);
101+
}
94102
}
95103
}
96104

@@ -142,15 +150,10 @@ public function getRequest()
142150
* Return the value of the promise (fulfilled).
143151
*
144152
* @return ResponseInterface Response Object only when the Promise is fulfilled.
145-
*
146-
* @throws \LogicException When the promise is not fulfilled.
147153
*/
148154
public function getResponse()
149155
{
150-
if (null === $this->response) {
151-
throw new \LogicException('Promise is not fulfilled');
152-
}
153-
return $this->response;
156+
return $this->responseBuilder->getResponse();
154157
}
155158

156159
/**
@@ -168,19 +171,24 @@ public function getException()
168171
if (null === $this->exception) {
169172
throw new \LogicException('Promise is not rejected');
170173
}
174+
171175
return $this->exception;
172176
}
173177

174178
/**
175179
* Fulfill promise.
176180
*
177-
* @param ResponseInterface $response Received response
181+
* @throws \Exception from on fulfill handler.
178182
*/
179-
public function fulfill(ResponseInterface $response)
183+
public function fulfill()
180184
{
181-
$this->response = $response;
182185
$this->state = Promise::FULFILLED;
183-
$this->response = $this->call($this->onFulfilled, $this->response);
186+
$response = $this->responseBuilder->getResponse();
187+
$response->getBody()->seek(0);
188+
$response = $this->call($this->onFulfilled, $response);
189+
if ($response instanceof ResponseInterface) {
190+
$this->responseBuilder->setResponse($response);
191+
}
184192
}
185193

186194
/**
@@ -214,6 +222,7 @@ private function call(array &$callbacks, $argument)
214222
$callback = array_shift($callbacks);
215223
$argument = call_user_func($callback, $argument);
216224
}
225+
217226
return $argument;
218227
}
219228
}

src/ResponseBuilder.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
namespace Http\Client\Curl;
3+
4+
use Http\Message\Builder\ResponseBuilder as OriginalResponseBuilder;
5+
use Psr\Http\Message\ResponseInterface;
6+
7+
/**
8+
* Extended response builder
9+
*/
10+
class ResponseBuilder extends OriginalResponseBuilder
11+
{
12+
/**
13+
* Replace response with a new instance
14+
*
15+
* @param ResponseInterface $response
16+
*/
17+
public function setResponse(ResponseInterface $response)
18+
{
19+
$this->response = $response;
20+
}
21+
}

0 commit comments

Comments
 (0)