21 private $handles = [];
31 $this->maxHandles = $maxHandles;
36 if (isset($options[
'curl'][
'body_as_string'])) {
37 $options[
'_body_as_string'] = $options[
'curl'][
'body_as_string'];
38 unset($options[
'curl'][
'body_as_string']);
42 $easy->request = $request;
43 $easy->options = $options;
44 $conf = $this->getDefaultConf($easy);
45 $this->applyMethod($easy, $conf);
46 $this->applyHandlerOptions($easy, $conf);
47 $this->applyHeaders($easy, $conf);
48 unset($conf[
'_headers']);
51 if (isset($options[
'curl'])) {
52 $conf = array_replace($conf, $options[
'curl']);
55 $conf[CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
56 $easy->handle = $this->handles
57 ? array_pop($this->handles)
59 curl_setopt_array($easy->handle, $conf);
64 public function release(EasyHandle $easy)
66 $resource = $easy->handle;
69 if (count($this->handles) >= $this->maxHandles) {
70 curl_close($resource);
76 curl_setopt($resource, CURLOPT_HEADERFUNCTION,
null);
77 curl_setopt($resource, CURLOPT_READFUNCTION,
null);
78 curl_setopt($resource, CURLOPT_WRITEFUNCTION,
null);
79 curl_setopt($resource, CURLOPT_PROGRESSFUNCTION,
null);
80 curl_reset($resource);
81 $this->handles[] = $resource;
95 public static function finish(
98 CurlFactoryInterface $factory
100 if (isset($easy->options[
'on_stats'])) {
101 self::invokeStats($easy);
104 if (!$easy->response || $easy->errno) {
105 return self::finishError($handler, $easy, $factory);
109 $factory->release($easy);
112 $body = $easy->response->getBody();
113 if ($body->isSeekable()) {
117 return new FulfilledPromise($easy->response);
120 private static function invokeStats(EasyHandle $easy)
122 $curlStats = curl_getinfo($easy->handle);
123 $curlStats[
'appconnect_time'] = curl_getinfo($easy->handle, CURLINFO_APPCONNECT_TIME);
124 $stats =
new TransferStats(
127 $curlStats[
'total_time'],
131 call_user_func($easy->options[
'on_stats'], $stats);
134 private static function finishError(
137 CurlFactoryInterface $factory
141 'errno' => $easy->errno,
142 'error' => curl_error($easy->handle),
143 'appconnect_time' => curl_getinfo($easy->handle, CURLINFO_APPCONNECT_TIME),
144 ] + curl_getinfo($easy->handle);
146 $factory->release($easy);
149 if (empty($easy->options[
'_err_message'])
150 && (!$easy->errno || $easy->errno == 65)
152 return self::retryFailedRewind($handler, $easy, $ctx);
155 return self::createRejection($easy, $ctx);
158 private static function createRejection(EasyHandle $easy, array $ctx)
160 static $connectionErrors = [
161 CURLE_OPERATION_TIMEOUTED =>
true,
162 CURLE_COULDNT_RESOLVE_HOST =>
true,
163 CURLE_COULDNT_CONNECT =>
true,
164 CURLE_SSL_CONNECT_ERROR =>
true,
165 CURLE_GOT_NOTHING =>
true,
170 if ($easy->onHeadersException) {
171 return \GuzzleHttp\Promise\rejection_for(
172 new RequestException(
173 'An error was encountered during the on_headers event',
176 $easy->onHeadersException,
181 if (version_compare($ctx[self::CURL_VERSION_STR], self::LOW_CURL_VERSION_NUMBER)) {
183 'cURL error %s: %s (%s)',
186 'see https://curl.haxx.se/libcurl/c/libcurl-errors.html'
190 'cURL error %s: %s (%s) for %s',
193 'see https://curl.haxx.se/libcurl/c/libcurl-errors.html',
194 $easy->request->getUri()
199 $error = isset($connectionErrors[$easy->errno])
200 ?
new ConnectException($message, $easy->request,
null, $ctx)
201 : new RequestException($message, $easy->request, $easy->response, null, $ctx);
203 return \GuzzleHttp\Promise\rejection_for($error);
206 private function getDefaultConf(EasyHandle $easy)
209 '_headers' => $easy->request->getHeaders(),
210 CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(),
211 CURLOPT_URL => (string) $easy->request->getUri()->withFragment(
''),
212 CURLOPT_RETURNTRANSFER =>
false,
213 CURLOPT_HEADER =>
false,
214 CURLOPT_CONNECTTIMEOUT => 150,
217 if (defined(
'CURLOPT_PROTOCOLS')) {
218 $conf[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
221 $version = $easy->request->getProtocolVersion();
222 if ($version == 1.1) {
223 $conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
224 } elseif ($version == 2.0) {
225 $conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
227 $conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
233 private function applyMethod(EasyHandle $easy, array &$conf)
235 $body = $easy->request->getBody();
236 $size = $body->getSize();
238 if ($size ===
null || $size > 0) {
239 $this->applyBody($easy->request, $easy->options, $conf);
243 $method = $easy->request->getMethod();
244 if ($method ===
'PUT' || $method ===
'POST') {
246 if (!$easy->request->hasHeader(
'Content-Length')) {
247 $conf[CURLOPT_HTTPHEADER][] =
'Content-Length: 0';
249 } elseif ($method ===
'HEAD') {
250 $conf[CURLOPT_NOBODY] =
true;
252 $conf[CURLOPT_WRITEFUNCTION],
253 $conf[CURLOPT_READFUNCTION],
255 $conf[CURLOPT_INFILE]
260 private function applyBody(RequestInterface $request, array $options, array &$conf)
262 $size = $request->hasHeader(
'Content-Length')
263 ? (int) $request->getHeaderLine(
'Content-Length')
268 if (($size !==
null && $size < 1000000) ||
269 !empty($options[
'_body_as_string'])
271 $conf[CURLOPT_POSTFIELDS] = (string) $request->getBody();
273 $this->removeHeader(
'Content-Length', $conf);
274 $this->removeHeader(
'Transfer-Encoding', $conf);
276 $conf[CURLOPT_UPLOAD] =
true;
277 if ($size !==
null) {
278 $conf[CURLOPT_INFILESIZE] = $size;
279 $this->removeHeader(
'Content-Length', $conf);
281 $body = $request->getBody();
282 if ($body->isSeekable()) {
285 $conf[CURLOPT_READFUNCTION] =
function ($ch, $fd, $length) use ($body) {
286 return $body->read($length);
291 if (!$request->hasHeader(
'Expect')) {
292 $conf[CURLOPT_HTTPHEADER][] =
'Expect:';
296 if (!$request->hasHeader(
'Content-Type')) {
297 $conf[CURLOPT_HTTPHEADER][] =
'Content-Type:';
301 private function applyHeaders(EasyHandle $easy, array &$conf)
303 foreach ($conf[
'_headers'] as $name => $values) {
304 foreach ($values as $value) {
305 $value = (string) $value;
309 $conf[CURLOPT_HTTPHEADER][] =
"$name;";
311 $conf[CURLOPT_HTTPHEADER][] =
"$name: $value";
317 if (!$easy->request->hasHeader(
'Accept')) {
318 $conf[CURLOPT_HTTPHEADER][] =
'Accept:';
328 private function removeHeader($name, array &$options)
330 foreach (array_keys($options[
'_headers']) as $key) {
331 if (!strcasecmp($key, $name)) {
332 unset($options[
'_headers'][$key]);
338 private function applyHandlerOptions(EasyHandle $easy, array &$conf)
340 $options = $easy->options;
341 if (isset($options[
'verify'])) {
342 if ($options[
'verify'] ===
false) {
343 unset($conf[CURLOPT_CAINFO]);
344 $conf[CURLOPT_SSL_VERIFYHOST] = 0;
345 $conf[CURLOPT_SSL_VERIFYPEER] =
false;
347 $conf[CURLOPT_SSL_VERIFYHOST] = 2;
348 $conf[CURLOPT_SSL_VERIFYPEER] =
true;
349 if (is_string($options[
'verify'])) {
351 if (!file_exists($options[
'verify'])) {
352 throw new \InvalidArgumentException(
353 "SSL CA bundle not found: {$options['verify']}"
358 if (is_dir($options[
'verify']) ||
359 (is_link($options[
'verify']) && is_dir(readlink($options[
'verify'])))) {
360 $conf[CURLOPT_CAPATH] = $options[
'verify'];
362 $conf[CURLOPT_CAINFO] = $options[
'verify'];
368 if (!empty($options[
'decode_content'])) {
369 $accept = $easy->request->getHeaderLine(
'Accept-Encoding');
371 $conf[CURLOPT_ENCODING] = $accept;
373 $conf[CURLOPT_ENCODING] =
'';
375 $conf[CURLOPT_HTTPHEADER][] =
'Accept-Encoding:';
379 if (isset($options[
'sink'])) {
380 $sink = $options[
'sink'];
381 if (!is_string($sink)) {
382 $sink = \GuzzleHttp\Psr7\stream_for($sink);
383 } elseif (!is_dir(dirname($sink))) {
385 throw new \RuntimeException(sprintf(
386 'Directory %s does not exist for sink value of %s',
391 $sink =
new LazyOpenStream($sink,
'w+');
394 $conf[CURLOPT_WRITEFUNCTION] =
function ($ch, $write) use ($sink) {
395 return $sink->write($write);
399 $conf[CURLOPT_FILE] = fopen(
'php://temp',
'w+');
402 $timeoutRequiresNoSignal =
false;
403 if (isset($options[
'timeout'])) {
404 $timeoutRequiresNoSignal |= $options[
'timeout'] < 1;
405 $conf[CURLOPT_TIMEOUT_MS] = $options[
'timeout'] * 1000;
409 if (isset($options[
'force_ip_resolve'])) {
410 if (
'v4' === $options[
'force_ip_resolve']) {
411 $conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4;
412 } elseif (
'v6' === $options[
'force_ip_resolve']) {
413 $conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V6;
417 if (isset($options[
'connect_timeout'])) {
418 $timeoutRequiresNoSignal |= $options[
'connect_timeout'] < 1;
419 $conf[CURLOPT_CONNECTTIMEOUT_MS] = $options[
'connect_timeout'] * 1000;
422 if ($timeoutRequiresNoSignal && strtoupper(substr(PHP_OS, 0, 3)) !==
'WIN') {
423 $conf[CURLOPT_NOSIGNAL] =
true;
426 if (isset($options[
'proxy'])) {
427 if (!is_array($options[
'proxy'])) {
428 $conf[CURLOPT_PROXY] = $options[
'proxy'];
430 $scheme = $easy->request->getUri()->getScheme();
431 if (isset($options[
'proxy'][$scheme])) {
432 $host = $easy->request->getUri()->getHost();
433 if (!isset($options[
'proxy'][
'no']) ||
436 $conf[CURLOPT_PROXY] = $options[
'proxy'][$scheme];
442 if (isset($options[
'cert'])) {
443 $cert = $options[
'cert'];
444 if (is_array($cert)) {
445 $conf[CURLOPT_SSLCERTPASSWD] = $cert[1];
448 if (!file_exists($cert)) {
449 throw new \InvalidArgumentException(
450 "SSL certificate not found: {$cert}"
453 $conf[CURLOPT_SSLCERT] = $cert;
456 if (isset($options[
'ssl_key'])) {
457 if (is_array($options[
'ssl_key'])) {
458 if (count($options[
'ssl_key']) === 2) {
459 list($sslKey, $conf[CURLOPT_SSLKEYPASSWD]) = $options[
'ssl_key'];
461 list($sslKey) = $options[
'ssl_key'];
465 $sslKey = isset($sslKey) ? $sslKey: $options[
'ssl_key'];
467 if (!file_exists($sslKey)) {
468 throw new \InvalidArgumentException(
469 "SSL private key not found: {$sslKey}"
472 $conf[CURLOPT_SSLKEY] = $sslKey;
475 if (isset($options[
'progress'])) {
476 $progress = $options[
'progress'];
477 if (!is_callable($progress)) {
478 throw new \InvalidArgumentException(
479 'progress client option must be callable'
482 $conf[CURLOPT_NOPROGRESS] =
false;
483 $conf[CURLOPT_PROGRESSFUNCTION] =
function () use ($progress) {
484 $args = func_get_args();
486 if (is_resource($args[0])) {
489 call_user_func_array($progress, $args);
493 if (!empty($options[
'debug'])) {
494 $conf[CURLOPT_STDERR] = \GuzzleHttp\debug_resource($options[
'debug']);
495 $conf[CURLOPT_VERBOSE] =
true;
508 private static function retryFailedRewind(
515 $body = $easy->request->getBody();
516 if ($body->tell() > 0) {
519 }
catch (\RuntimeException $e) {
520 $ctx[
'error'] =
'The connection unexpectedly failed without '
521 .
'providing an error. The request would have been retried, '
522 .
'but attempting to rewind the request body failed. '
523 .
'Exception: ' . $e;
524 return self::createRejection($easy, $ctx);
528 if (!isset($easy->options[
'_curl_retries'])) {
529 $easy->options[
'_curl_retries'] = 1;
530 } elseif ($easy->options[
'_curl_retries'] == 2) {
531 $ctx[
'error'] =
'The cURL request was retried 3 times '
532 .
'and did not succeed. The most likely reason for the failure '
533 .
'is that cURL was unable to rewind the body of the request '
534 .
'and subsequent retries resulted in the same error. Turn on '
535 .
'the debug option to see what went wrong. See '
536 .
'https://bugs.php.net/bug.php?id=47204 for more information.';
537 return self::createRejection($easy, $ctx);
539 $easy->options[
'_curl_retries']++;
542 return $handler($easy->request, $easy->options);
545 private function createHeaderFn(EasyHandle $easy)
547 if (isset($easy->options[
'on_headers'])) {
548 $onHeaders = $easy->options[
'on_headers'];
550 if (!is_callable($onHeaders)) {
551 throw new \InvalidArgumentException(
'on_headers must be callable');
557 return function ($ch, $h) use (
564 $startingResponse =
true;
565 $easy->createResponse();
566 if ($onHeaders !==
null) {
568 $onHeaders($easy->response);
569 }
catch (\Exception $e) {
572 $easy->onHeadersException = $e;
576 } elseif ($startingResponse) {
577 $startingResponse =
false;
578 $easy->headers = [$value];
580 $easy->headers[] = $value;