namespace GuzzleHttp\Handler;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Psr7\LazyOpenStream;
use GuzzleHttp\TransferStats;
use Psr\Http\Message\RequestInterface;
* Creates curl resources from a request
class CurlFactory implements CurlFactoryInterface
const CURL_VERSION_STR = 'curl_version';
const LOW_CURL_VERSION_NUMBER = '7.21.2';
/** @var int Total number of idle handles to keep in cache */
* @param int $maxHandles Maximum number of idle handles.
public function __construct($maxHandles)
$this->maxHandles = $maxHandles;
public function create(RequestInterface $request, array $options)
if (isset($options['curl']['body_as_string'])) {
$options['_body_as_string'] = $options['curl']['body_as_string'];
unset($options['curl']['body_as_string']);
$easy->request = $request;
$easy->options = $options;
$conf = $this->getDefaultConf($easy);
$this->applyMethod($easy, $conf);
$this->applyHandlerOptions($easy, $conf);
$this->applyHeaders($easy, $conf);
unset($conf['_headers']);
// Add handler options from the request configuration options
if (isset($options['curl'])) {
$conf = array_replace($conf, $options['curl']);
$conf[CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
$easy->handle = $this->handles
? array_pop($this->handles)
curl_setopt_array($easy->handle, $conf);
public function release(EasyHandle $easy)
$resource = $easy->handle;
if (count($this->handles) >= $this->maxHandles) {
// Remove all callback functions as they can hold onto references
// and are not cleaned up by curl_reset. Using curl_setopt_array
// does not work for some reason, so removing each one
curl_setopt($resource, CURLOPT_HEADERFUNCTION, null);
curl_setopt($resource, CURLOPT_READFUNCTION, null);
curl_setopt($resource, CURLOPT_WRITEFUNCTION, null);
curl_setopt($resource, CURLOPT_PROGRESSFUNCTION, null);
$this->handles[] = $resource;
* Completes a cURL transaction, either returning a response promise or a
* @param callable $handler
* @param EasyHandle $easy
* @param CurlFactoryInterface $factory Dictates how the handle is released
* @return \GuzzleHttp\Promise\PromiseInterface
public static function finish(
CurlFactoryInterface $factory
if (isset($easy->options['on_stats'])) {
self::invokeStats($easy);
if (!$easy->response || $easy->errno) {
return self::finishError($handler, $easy, $factory);
// Return the response if it is present and there is no error.
$factory->release($easy);
// Rewind the body of the response if possible.
$body = $easy->response->getBody();
if ($body->isSeekable()) {
return new FulfilledPromise($easy->response);
private static function invokeStats(EasyHandle $easy)
$curlStats = curl_getinfo($easy->handle);
$curlStats['appconnect_time'] = curl_getinfo($easy->handle, CURLINFO_APPCONNECT_TIME);
$stats = new TransferStats(
$curlStats['total_time'],
call_user_func($easy->options['on_stats'], $stats);
private static function finishError(
CurlFactoryInterface $factory
// Get error information and release the handle to the factory.
'error' => curl_error($easy->handle),
'appconnect_time' => curl_getinfo($easy->handle, CURLINFO_APPCONNECT_TIME),
] + curl_getinfo($easy->handle);
$ctx[self::CURL_VERSION_STR] = curl_version()['version'];
$factory->release($easy);
// Retry when nothing is present or when curl failed to rewind.
if (empty($easy->options['_err_message'])
&& (!$easy->errno || $easy->errno == 65)
return self::retryFailedRewind($handler, $easy, $ctx);
return self::createRejection($easy, $ctx);
private static function createRejection(EasyHandle $easy, array $ctx)
static $connectionErrors = [
CURLE_OPERATION_TIMEOUTED => true,
CURLE_COULDNT_RESOLVE_HOST => true,
CURLE_COULDNT_CONNECT => true,
CURLE_SSL_CONNECT_ERROR => true,
CURLE_GOT_NOTHING => true,
// If an exception was encountered during the onHeaders event, then
// return a rejected promise that wraps that exception.
if ($easy->onHeadersException) {
return \GuzzleHttp\Promise\rejection_for(
'An error was encountered during the on_headers event',
$easy->onHeadersException,
if (version_compare($ctx[self::CURL_VERSION_STR], self::LOW_CURL_VERSION_NUMBER)) {
'cURL error %s: %s (%s)',
'see https://curl.haxx.se/libcurl/c/libcurl-errors.html'
'cURL error %s: %s (%s) for %s',
'see https://curl.haxx.se/libcurl/c/libcurl-errors.html',
// Create a connection exception if it was a specific error code.
$error = isset($connectionErrors[$easy->errno])
? new ConnectException($message, $easy->request, null, $ctx)
: new RequestException($message, $easy->request, $easy->response, null, $ctx);
return \GuzzleHttp\Promise\rejection_for($error);
private function getDefaultConf(EasyHandle $easy)
'_headers' => $easy->request->getHeaders(),
CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(),
CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''),
CURLOPT_RETURNTRANSFER => false,
CURLOPT_CONNECTTIMEOUT => 150,
if (defined('CURLOPT_PROTOCOLS')) {
$conf[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
$version = $easy->request->getProtocolVersion();
$conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
} elseif ($version == 2.0) {
$conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
$conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
private function applyMethod(EasyHandle $easy, array &$conf)
$body = $easy->request->getBody();
$size = $body->getSize();
if ($size === null || $size > 0) {
$this->applyBody($easy->request, $easy->options, $conf);
$method = $easy->request->getMethod();
if ($method === 'PUT' || $method === 'POST') {
// See http://tools.ietf.org/html/rfc7230#section-3.3.2
if (!$easy->request->hasHeader('Content-Length')) {
$conf[CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
} elseif ($method === 'HEAD') {
$conf[CURLOPT_NOBODY] = true;
$conf[CURLOPT_WRITEFUNCTION],
$conf[CURLOPT_READFUNCTION],
private function applyBody(RequestInterface $request, array $options, array &$conf)
$size = $request->hasHeader('Content-Length')
? (int) $request->getHeaderLine('Content-Length')
// Send the body as a string if the size is less than 1MB OR if the
// [curl][body_as_string] request value is set.
if (($size !== null && $size < 1000000) ||
!empty($options['_body_as_string'])
$conf[CURLOPT_POSTFIELDS] = (string) $request->getBody();
// Don't duplicate the Content-Length header
$this->removeHeader('Content-Length', $conf);
$this->removeHeader('Transfer-Encoding', $conf);
$conf[CURLOPT_UPLOAD] = true;
$conf[CURLOPT_INFILESIZE] = $size;
$this->removeHeader('Content-Length', $conf);
$body = $request->getBody();
if ($body->isSeekable()) {
$conf[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) {
return $body->read($length);
// If the Expect header is not present, prevent curl from adding it
if (!$request->hasHeader('Expect')) {
$conf[CURLOPT_HTTPHEADER][] = 'Expect:';
// cURL sometimes adds a content-type by default. Prevent this.
if (!$request->hasHeader('Content-Type')) {
$conf[CURLOPT_HTTPHEADER][] = 'Content-Type:';
private function applyHeaders(EasyHandle $easy, array &$conf)
foreach ($conf['_headers'] as $name => $values) {
foreach ($values as $value) {
$value = (string) $value;
// cURL requires a special format for empty headers.
// See https://github.com/guzzle/guzzle/issues/1882 for more details.
$conf[CURLOPT_HTTPHEADER][] = "$name;";
$conf[CURLOPT_HTTPHEADER][] = "$name: $value";
// Remove the Accept header if one was not set
if (!$easy->request->hasHeader('Accept')) {
$conf[CURLOPT_HTTPHEADER][] = 'Accept:';
* Remove a header from the options array.
* @param string $name Case-insensitive header to remove
* @param array $options Array of options to modify
private function removeHeader($name, array &$options)
foreach (array_keys($options['_headers']) as $key) {
if (!strcasecmp($key, $name)) {
unset($options['_headers'][$key]);
private function applyHandlerOptions(EasyHandle $easy, array &$conf)
$options = $easy->options;
if (isset($options['verify'])) {
if ($options['verify'] === false) {
unset($conf[CURLOPT_CAINFO]);
$conf[CURLOPT_SSL_VERIFYHOST] = 0;
$conf[CURLOPT_SSL_VERIFYPEER] = false;
$conf[CURLOPT_SSL_VERIFYHOST] = 2;
$conf[CURLOPT_SSL_VERIFYPEER] = true;
if (is_string($options['verify'])) {
// Throw an error if the file/folder/link path is not valid or doesn't exist.
if (!file_exists($options['verify'])) {
throw new \InvalidArgumentException(
"SSL CA bundle not found: {$options['verify']}"
// If it's a directory or a link to a directory use CURLOPT_CAPATH.
// If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO.
if (is_dir($options['verify']) ||
(is_link($options['verify']) && is_dir(readlink($options['verify'])))) {
$conf[CURLOPT_CAPATH] = $options['verify'];
$conf[CURLOPT_CAINFO] = $options['verify'];
if (!empty($options['decode_content'])) {
$accept = $easy->request->getHeaderLine('Accept-Encoding');
$conf[CURLOPT_ENCODING] = $accept;
$conf[CURLOPT_ENCODING] = '';
// Don't let curl send the header over the wire
$conf[CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
if (isset($options['sink'])) {
$sink = $options['sink'];
$sink = \GuzzleHttp\Psr7\stream_for($sink);
} elseif (!is_dir(dirname($sink))) {
// Ensure that the directory exists before failing in curl.
throw new \RuntimeException(sprintf(
'Directory %s does not exist for sink value of %s',
$sink = new LazyOpenStream($sink, 'w+');
$conf[CURLOPT_WRITEFUNCTION] = function ($ch, $write) use ($sink) {
return $sink->write($write);
// Use a default temp stream if no sink was set.
$conf[CURLOPT_FILE] = fopen('php://temp', 'w+');
$easy->sink = Psr7\stream_for($conf[CURLOPT_FILE]);
$timeoutRequiresNoSignal = false;
if (isset($options['timeout'])) {
$timeoutRequiresNoSignal |= $options['timeout'] < 1;
$conf[CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
// CURL default value is CURL_IPRESOLVE_WHATEVER
if (isset($options['force_ip_resolve'])) {
if ('v4' === $options['force_ip_resolve']) {
$conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4;
} elseif ('v6' === $options['force_ip_resolve']) {
$conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V6;
if (isset($options['connect_timeout'])) {
$timeoutRequiresNoSignal |= $options['connect_timeout'] < 1;
$conf[CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
if ($timeoutRequiresNoSignal && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
$conf[CURLOPT_NOSIGNAL] = true;
if (isset($options['proxy'])) {
if (!is_array($options['proxy'])) {
$conf[CURLOPT_PROXY] = $options['proxy'];
$scheme = $easy->request->getUri()->getScheme();
if (isset($options['proxy'][$scheme])) {
$host = $easy->request->getUri()->getHost();
if (!isset($options['proxy']['no']) ||
!\GuzzleHttp\is_host_in_noproxy($host, $options['proxy']['no'])
$conf[CURLOPT_PROXY] = $options['proxy'][$scheme];
if (isset($options['cert'])) {
$cert = $options['cert'];
$conf[CURLOPT_SSLCERTPASSWD] = $cert[1];
if (!file_exists($cert)) {
throw new \InvalidArgumentException(
"SSL certificate not found: {$cert}"
$conf[CURLOPT_SSLCERT] = $cert;
if (isset($options['ssl_key'])) {
if (is_array($options['ssl_key'])) {
if (count($options['ssl_key']) === 2) {
list($sslKey, $conf[CURLOPT_SSLKEYPASSWD]) = $options['ssl_key'];
list($sslKey) = $options['ssl_key'];
$sslKey = isset($sslKey) ? $sslKey: $options['ssl_key'];
if (!file_exists($sslKey)) {
throw new \InvalidArgumentException(
"SSL private key not found: {$sslKey}"
$conf[CURLOPT_SSLKEY] = $sslKey;
if (isset($options['progress'])) {
$progress = $options['progress'];
if (!is_callable($progress)) {
throw new \InvalidArgumentException(
'progress client option must be callable'
$conf[CURLOPT_NOPROGRESS] = false;
$conf[CURLOPT_PROGRESSFUNCTION] = function () use ($progress) {
// PHP 5.5 pushed the handle onto the start of the args
if (is_resource($args[0])) {
call_user_func_array($progress, $args);
if (!empty($options['debug'])) {
$conf[CURLOPT_STDERR] = \GuzzleHttp\debug_resource($options['debug']);
$conf[CURLOPT_VERBOSE] = true;
* This function ensures that a response was set on a transaction. If one