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;