3
namespace GuzzleHttp\Adapter\Curl;
5
use GuzzleHttp\Adapter\AdapterInterface;
6
use GuzzleHttp\Adapter\ParallelAdapterInterface;
7
use GuzzleHttp\Adapter\TransactionInterface;
8
use GuzzleHttp\Event\RequestEvents;
9
use GuzzleHttp\Exception\AdapterException;
10
use GuzzleHttp\Exception\RequestException;
11
use GuzzleHttp\Message\MessageFactoryInterface;
14
* HTTP adapter that uses cURL multi as a transport layer
16
* When using the CurlAdapter, custom curl options can be specified as an
17
* associative array of curl option constants mapping to values in the
18
* **curl** key of a request's configuration options.
20
* In addition to being able to supply configuration options via the curl
21
* request config, you can also specify the select_timeout variable using the
22
* `GUZZLE_CURL_SELECT_TIMEOUT` environment variable.
24
class MultiAdapter implements AdapterInterface, ParallelAdapterInterface
26
const ERROR_STR = 'See http://curl.haxx.se/libcurl/c/libcurl-errors.html for an explanation of cURL errors';
27
const ENV_SELECT_TIMEOUT = 'GUZZLE_CURL_SELECT_TIMEOUT';
29
/** @var CurlFactory */
32
/** @var MessageFactoryInterface */
33
private $messageFactory;
35
/** @var array Array of curl multi handles */
36
private $multiHandles = [];
38
/** @var array Array of curl multi handles */
39
private $multiOwned = [];
41
/** @var int Total number of idle handles to keep in cache */
45
private $selectTimeout;
48
* Accepts an associative array of options:
50
* - handle_factory: Optional callable factory used to create cURL handles.
51
* The callable is invoked with the following arguments:
52
* TransactionInterface, MessageFactoryInterface, and an optional cURL
53
* handle to modify. The factory method must then return a cURL resource.
54
* - select_timeout: Specify a float in seconds to use for a
55
* curl_multi_select timeout.
56
* - max_handles: Maximum number of idle handles (defaults to 3).
58
* @param MessageFactoryInterface $messageFactory
59
* @param array $options Array of options to use with the adapter:
61
public function __construct(
62
MessageFactoryInterface $messageFactory,
65
$this->messageFactory = $messageFactory;
66
$this->curlFactory = isset($options['handle_factory'])
67
? $options['handle_factory']
70
if (isset($options['select_timeout'])) {
71
$this->selectTimeout = $options['select_timeout'];
72
} elseif (isset($_SERVER[self::ENV_SELECT_TIMEOUT])) {
73
$this->selectTimeout = $_SERVER[self::ENV_SELECT_TIMEOUT];
75
$this->selectTimeout = 1;
78
$this->maxHandles = isset($options['max_handles'])
79
? $options['max_handles']
83
public function __destruct()
85
foreach ($this->multiHandles as $handle) {
86
if (is_resource($handle)) {
87
curl_multi_close($handle);
93
* Throw an exception for a cURL multi response
95
* @param int $code Curl response code
96
* @throws AdapterException
98
public static function throwMultiError($code)
100
$buffer = function_exists('curl_multi_strerror')
101
? curl_multi_strerror($code)
104
throw new AdapterException(sprintf('cURL error %s: %s', $code, $buffer));
107
public function send(TransactionInterface $transaction)
109
$context = new BatchContext($this->checkoutMultiHandle(), true);
110
$this->addHandle($transaction, $context);
111
$this->perform($context);
113
return $transaction->getResponse();
116
public function sendAll(\Iterator $transactions, $parallel)
118
$context = new BatchContext(
119
$this->checkoutMultiHandle(),
124
foreach (new \LimitIterator($transactions, 0, $parallel) as $trans) {
125
$this->addHandle($trans, $context);
128
$this->perform($context);
131
private function perform(BatchContext $context)
133
// The first curl_multi_select often times out no matter what, but is
134
// usually required for fast transfers.
136
$multi = $context->getMultiHandle();
140
$mrc = curl_multi_exec($multi, $active);
141
} while ($mrc === CURLM_CALL_MULTI_PERFORM);
143
if ($mrc != CURLM_OK) {
144
self::throwMultiError($mrc);
147
$this->processMessages($context);
150
curl_multi_select($multi, $this->selectTimeout) === -1
152
// Perform a usleep if a select returns -1.
153
// See: https://bugs.php.net/bug.php?id=61141
157
} while ($context->isActive() || $active);
159
$this->releaseMultiHandle($multi, $this->maxHandles);
162
private function processMessages(BatchContext $context)
164
$multi = $context->getMultiHandle();
166
while ($done = curl_multi_info_read($multi)) {
167
$transaction = $context->findTransaction($done['handle']);
168
$this->processResponse($transaction, $done, $context);
169
// Add the next transaction if there are more in the queue
170
if ($next = $context->nextPending()) {
171
$this->addHandle($next, $context);
176
private function processResponse(
177
TransactionInterface $transaction,
179
BatchContext $context
181
$info = $context->removeTransaction($transaction);
184
if (!$this->isCurlException($transaction, $curl, $context, $info) &&
185
$this->validateResponseWasSet($transaction, $context)
187
if ($body = $transaction->getResponse()->getBody()) {
190
RequestEvents::emitComplete($transaction, $info);
192
} catch (\Exception $e) {
193
$this->throwException($e, $context);
197
private function addHandle(
198
TransactionInterface $transaction,
199
BatchContext $context
202
RequestEvents::emitBefore($transaction);
203
// Only transfer if the request was not intercepted
204
if (!$transaction->getResponse()) {
205
$factory = $this->curlFactory;
206
$context->addTransaction(
208
$factory($transaction, $this->messageFactory)
211
} catch (RequestException $e) {
212
$this->throwException($e, $context);
216
private function isCurlException(
217
TransactionInterface $transaction,
219
BatchContext $context,
222
if (CURLM_OK == $curl['result'] ||
223
CURLM_CALL_MULTI_PERFORM == $curl['result']
228
$request = $transaction->getRequest();
230
// Send curl stats along if they are available
231
$stats = ['curl_result' => $curl['result']] + $info;
232
RequestEvents::emitError(
234
new RequestException(
236
'[curl] (#%s) %s [url] %s',
238
function_exists('curl_strerror')
239
? curl_strerror($curl['result'])
247
} catch (\Exception $e) {
248
$this->throwException($e, $context);
254
private function throwException(\Exception $e, BatchContext $context)
256
if ($context->throwsExceptions()
257
|| ($e instanceof RequestException && $e->getThrowImmediately())
259
$context->removeAll();
260
$this->releaseMultiHandle($context->getMultiHandle(), -1);
266
* Returns a curl_multi handle from the cache or creates a new one
270
private function checkoutMultiHandle()
272
// Find an unused handle in the cache
273
$key = array_search(false, $this->multiOwned, true);
274
if (false !== $key) {
275
$this->multiOwned[$key] = true;
276
return $this->multiHandles[$key];
280
$handle = curl_multi_init();
282
$this->multiHandles[$id] = $handle;
283
$this->multiOwned[$id] = true;
289
* Releases a curl_multi handle back into the cache and removes excess cache
291
* @param resource $handle Curl multi handle to remove
292
* @param int $maxHandles (Optional) Maximum number of existing multiHandles to allow before pruning.
294
private function releaseMultiHandle($handle, $maxHandles)
298
if (count($this->multiHandles) <= $maxHandles) {
299
$this->multiOwned[$id] = false;
300
} elseif (isset($this->multiHandles[$id], $this->multiOwned[$id])) {
301
// Prune excessive handles
302
curl_multi_close($this->multiHandles[$id]);
303
unset($this->multiHandles[$id], $this->multiOwned[$id]);
308
* This function ensures that a response was set on a transaction. If one
309
* was not set, then the request is retried if possible. This error
310
* typically means you are sending a payload, curl encountered a
311
* "Connection died, retrying a fresh connect" error, tried to rewind the
312
* stream, and then encountered a "necessary data rewind wasn't possible"
313
* error, causing the request to be sent through curl_multi_info_read()
314
* without an error status.
316
* @param TransactionInterface $transaction
317
* @param BatchContext $context
319
* @return bool Returns true if it's OK, and false if it failed.
320
* @throws \GuzzleHttp\Exception\RequestException If it failed and cannot
323
private function validateResponseWasSet(
324
TransactionInterface $transaction,
325
BatchContext $context
327
if ($transaction->getResponse()) {
331
$body = $transaction->getRequest()->getBody();
334
// This is weird and should probably never happen.
335
RequestEvents::emitError(
337
new RequestException(
338
'No response was received for a request with no body. This'
339
. ' could mean that you are saturating your network.',
340
$transaction->getRequest()
343
} elseif (!$body->isSeekable() || !$body->seek(0)) {
344
// Nothing we can do with this. Sorry!
345
RequestEvents::emitError(
347
new RequestException(
348
'The connection was unexpectedly closed. The request would'
349
. ' have been retried, but attempting to rewind the'
350
. ' request body failed. Consider wrapping your request'
351
. ' body in a CachingStream decorator to work around this'
352
. ' issue if necessary.',
353
$transaction->getRequest()
357
$this->retryFailedConnection($transaction, $context);
363
private function retryFailedConnection(
364
TransactionInterface $transaction,
365
BatchContext $context
367
// Add the request back to the batch to retry automatically.
368
$context->addTransaction(
373
$this->messageFactory