CurlFactory.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  1. <?php
  2. namespace GuzzleHttp\Handler;
  3. use GuzzleHttp\Exception\ConnectException;
  4. use GuzzleHttp\Exception\RequestException;
  5. use GuzzleHttp\Promise as P;
  6. use GuzzleHttp\Promise\FulfilledPromise;
  7. use GuzzleHttp\Promise\PromiseInterface;
  8. use GuzzleHttp\Psr7\LazyOpenStream;
  9. use GuzzleHttp\TransferStats;
  10. use GuzzleHttp\Utils;
  11. use Psr\Http\Message\RequestInterface;
  12. /**
  13. * Creates curl resources from a request
  14. *
  15. * @final
  16. */
  17. class CurlFactory implements CurlFactoryInterface
  18. {
  19. public const CURL_VERSION_STR = 'curl_version';
  20. /**
  21. * @deprecated
  22. */
  23. public const LOW_CURL_VERSION_NUMBER = '7.21.2';
  24. /**
  25. * @var resource[]|\CurlHandle[]
  26. */
  27. private $handles = [];
  28. /**
  29. * @var int Total number of idle handles to keep in cache
  30. */
  31. private $maxHandles;
  32. /**
  33. * @param int $maxHandles Maximum number of idle handles.
  34. */
  35. public function __construct(int $maxHandles)
  36. {
  37. $this->maxHandles = $maxHandles;
  38. }
  39. public function create(RequestInterface $request, array $options): EasyHandle
  40. {
  41. if (isset($options['curl']['body_as_string'])) {
  42. $options['_body_as_string'] = $options['curl']['body_as_string'];
  43. unset($options['curl']['body_as_string']);
  44. }
  45. $easy = new EasyHandle();
  46. $easy->request = $request;
  47. $easy->options = $options;
  48. $conf = $this->getDefaultConf($easy);
  49. $this->applyMethod($easy, $conf);
  50. $this->applyHandlerOptions($easy, $conf);
  51. $this->applyHeaders($easy, $conf);
  52. unset($conf['_headers']);
  53. // Add handler options from the request configuration options
  54. if (isset($options['curl'])) {
  55. $conf = \array_replace($conf, $options['curl']);
  56. }
  57. $conf[\CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
  58. $easy->handle = $this->handles ? \array_pop($this->handles) : \curl_init();
  59. curl_setopt_array($easy->handle, $conf);
  60. return $easy;
  61. }
  62. public function release(EasyHandle $easy): void
  63. {
  64. $resource = $easy->handle;
  65. unset($easy->handle);
  66. if (\count($this->handles) >= $this->maxHandles) {
  67. \curl_close($resource);
  68. } else {
  69. // Remove all callback functions as they can hold onto references
  70. // and are not cleaned up by curl_reset. Using curl_setopt_array
  71. // does not work for some reason, so removing each one
  72. // individually.
  73. \curl_setopt($resource, \CURLOPT_HEADERFUNCTION, null);
  74. \curl_setopt($resource, \CURLOPT_READFUNCTION, null);
  75. \curl_setopt($resource, \CURLOPT_WRITEFUNCTION, null);
  76. \curl_setopt($resource, \CURLOPT_PROGRESSFUNCTION, null);
  77. \curl_reset($resource);
  78. $this->handles[] = $resource;
  79. }
  80. }
  81. /**
  82. * Completes a cURL transaction, either returning a response promise or a
  83. * rejected promise.
  84. *
  85. * @param callable(RequestInterface, array): PromiseInterface $handler
  86. * @param CurlFactoryInterface $factory Dictates how the handle is released
  87. */
  88. public static function finish(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface
  89. {
  90. if (isset($easy->options['on_stats'])) {
  91. self::invokeStats($easy);
  92. }
  93. if (!$easy->response || $easy->errno) {
  94. return self::finishError($handler, $easy, $factory);
  95. }
  96. // Return the response if it is present and there is no error.
  97. $factory->release($easy);
  98. // Rewind the body of the response if possible.
  99. $body = $easy->response->getBody();
  100. if ($body->isSeekable()) {
  101. $body->rewind();
  102. }
  103. return new FulfilledPromise($easy->response);
  104. }
  105. private static function invokeStats(EasyHandle $easy): void
  106. {
  107. $curlStats = \curl_getinfo($easy->handle);
  108. $curlStats['appconnect_time'] = \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME);
  109. $stats = new TransferStats(
  110. $easy->request,
  111. $easy->response,
  112. $curlStats['total_time'],
  113. $easy->errno,
  114. $curlStats
  115. );
  116. ($easy->options['on_stats'])($stats);
  117. }
  118. /**
  119. * @param callable(RequestInterface, array): PromiseInterface $handler
  120. */
  121. private static function finishError(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface
  122. {
  123. // Get error information and release the handle to the factory.
  124. $ctx = [
  125. 'errno' => $easy->errno,
  126. 'error' => \curl_error($easy->handle),
  127. 'appconnect_time' => \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME),
  128. ] + \curl_getinfo($easy->handle);
  129. $ctx[self::CURL_VERSION_STR] = \curl_version()['version'];
  130. $factory->release($easy);
  131. // Retry when nothing is present or when curl failed to rewind.
  132. if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65)) {
  133. return self::retryFailedRewind($handler, $easy, $ctx);
  134. }
  135. return self::createRejection($easy, $ctx);
  136. }
  137. private static function createRejection(EasyHandle $easy, array $ctx): PromiseInterface
  138. {
  139. static $connectionErrors = [
  140. \CURLE_OPERATION_TIMEOUTED => true,
  141. \CURLE_COULDNT_RESOLVE_HOST => true,
  142. \CURLE_COULDNT_CONNECT => true,
  143. \CURLE_SSL_CONNECT_ERROR => true,
  144. \CURLE_GOT_NOTHING => true,
  145. ];
  146. if ($easy->createResponseException) {
  147. return P\Create::rejectionFor(
  148. new RequestException(
  149. 'An error was encountered while creating the response',
  150. $easy->request,
  151. $easy->response,
  152. $easy->createResponseException,
  153. $ctx
  154. )
  155. );
  156. }
  157. // If an exception was encountered during the onHeaders event, then
  158. // return a rejected promise that wraps that exception.
  159. if ($easy->onHeadersException) {
  160. return P\Create::rejectionFor(
  161. new RequestException(
  162. 'An error was encountered during the on_headers event',
  163. $easy->request,
  164. $easy->response,
  165. $easy->onHeadersException,
  166. $ctx
  167. )
  168. );
  169. }
  170. $message = \sprintf(
  171. 'cURL error %s: %s (%s)',
  172. $ctx['errno'],
  173. $ctx['error'],
  174. 'see https://curl.haxx.se/libcurl/c/libcurl-errors.html'
  175. );
  176. $uriString = (string) $easy->request->getUri();
  177. if ($uriString !== '' && false === \strpos($ctx['error'], $uriString)) {
  178. $message .= \sprintf(' for %s', $uriString);
  179. }
  180. // Create a connection exception if it was a specific error code.
  181. $error = isset($connectionErrors[$easy->errno])
  182. ? new ConnectException($message, $easy->request, null, $ctx)
  183. : new RequestException($message, $easy->request, $easy->response, null, $ctx);
  184. return P\Create::rejectionFor($error);
  185. }
  186. /**
  187. * @return array<int|string, mixed>
  188. */
  189. private function getDefaultConf(EasyHandle $easy): array
  190. {
  191. $conf = [
  192. '_headers' => $easy->request->getHeaders(),
  193. \CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(),
  194. \CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''),
  195. \CURLOPT_RETURNTRANSFER => false,
  196. \CURLOPT_HEADER => false,
  197. \CURLOPT_CONNECTTIMEOUT => 300,
  198. ];
  199. if (\defined('CURLOPT_PROTOCOLS')) {
  200. $conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP | \CURLPROTO_HTTPS;
  201. }
  202. $version = $easy->request->getProtocolVersion();
  203. if ($version == 1.1) {
  204. $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
  205. } elseif ($version == 2.0) {
  206. $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
  207. } else {
  208. $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
  209. }
  210. return $conf;
  211. }
  212. private function applyMethod(EasyHandle $easy, array &$conf): void
  213. {
  214. $body = $easy->request->getBody();
  215. $size = $body->getSize();
  216. if ($size === null || $size > 0) {
  217. $this->applyBody($easy->request, $easy->options, $conf);
  218. return;
  219. }
  220. $method = $easy->request->getMethod();
  221. if ($method === 'PUT' || $method === 'POST') {
  222. // See https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
  223. if (!$easy->request->hasHeader('Content-Length')) {
  224. $conf[\CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
  225. }
  226. } elseif ($method === 'HEAD') {
  227. $conf[\CURLOPT_NOBODY] = true;
  228. unset(
  229. $conf[\CURLOPT_WRITEFUNCTION],
  230. $conf[\CURLOPT_READFUNCTION],
  231. $conf[\CURLOPT_FILE],
  232. $conf[\CURLOPT_INFILE]
  233. );
  234. }
  235. }
  236. private function applyBody(RequestInterface $request, array $options, array &$conf): void
  237. {
  238. $size = $request->hasHeader('Content-Length')
  239. ? (int) $request->getHeaderLine('Content-Length')
  240. : null;
  241. // Send the body as a string if the size is less than 1MB OR if the
  242. // [curl][body_as_string] request value is set.
  243. if (($size !== null && $size < 1000000) || !empty($options['_body_as_string'])) {
  244. $conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody();
  245. // Don't duplicate the Content-Length header
  246. $this->removeHeader('Content-Length', $conf);
  247. $this->removeHeader('Transfer-Encoding', $conf);
  248. } else {
  249. $conf[\CURLOPT_UPLOAD] = true;
  250. if ($size !== null) {
  251. $conf[\CURLOPT_INFILESIZE] = $size;
  252. $this->removeHeader('Content-Length', $conf);
  253. }
  254. $body = $request->getBody();
  255. if ($body->isSeekable()) {
  256. $body->rewind();
  257. }
  258. $conf[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body) {
  259. return $body->read($length);
  260. };
  261. }
  262. // If the Expect header is not present, prevent curl from adding it
  263. if (!$request->hasHeader('Expect')) {
  264. $conf[\CURLOPT_HTTPHEADER][] = 'Expect:';
  265. }
  266. // cURL sometimes adds a content-type by default. Prevent this.
  267. if (!$request->hasHeader('Content-Type')) {
  268. $conf[\CURLOPT_HTTPHEADER][] = 'Content-Type:';
  269. }
  270. }
  271. private function applyHeaders(EasyHandle $easy, array &$conf): void
  272. {
  273. foreach ($conf['_headers'] as $name => $values) {
  274. foreach ($values as $value) {
  275. $value = (string) $value;
  276. if ($value === '') {
  277. // cURL requires a special format for empty headers.
  278. // See https://github.com/guzzle/guzzle/issues/1882 for more details.
  279. $conf[\CURLOPT_HTTPHEADER][] = "$name;";
  280. } else {
  281. $conf[\CURLOPT_HTTPHEADER][] = "$name: $value";
  282. }
  283. }
  284. }
  285. // Remove the Accept header if one was not set
  286. if (!$easy->request->hasHeader('Accept')) {
  287. $conf[\CURLOPT_HTTPHEADER][] = 'Accept:';
  288. }
  289. }
  290. /**
  291. * Remove a header from the options array.
  292. *
  293. * @param string $name Case-insensitive header to remove
  294. * @param array $options Array of options to modify
  295. */
  296. private function removeHeader(string $name, array &$options): void
  297. {
  298. foreach (\array_keys($options['_headers']) as $key) {
  299. if (!\strcasecmp($key, $name)) {
  300. unset($options['_headers'][$key]);
  301. return;
  302. }
  303. }
  304. }
  305. private function applyHandlerOptions(EasyHandle $easy, array &$conf): void
  306. {
  307. $options = $easy->options;
  308. if (isset($options['verify'])) {
  309. if ($options['verify'] === false) {
  310. unset($conf[\CURLOPT_CAINFO]);
  311. $conf[\CURLOPT_SSL_VERIFYHOST] = 0;
  312. $conf[\CURLOPT_SSL_VERIFYPEER] = false;
  313. } else {
  314. $conf[\CURLOPT_SSL_VERIFYHOST] = 2;
  315. $conf[\CURLOPT_SSL_VERIFYPEER] = true;
  316. if (\is_string($options['verify'])) {
  317. // Throw an error if the file/folder/link path is not valid or doesn't exist.
  318. if (!\file_exists($options['verify'])) {
  319. throw new \InvalidArgumentException("SSL CA bundle not found: {$options['verify']}");
  320. }
  321. // If it's a directory or a link to a directory use CURLOPT_CAPATH.
  322. // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO.
  323. if (
  324. \is_dir($options['verify'])
  325. || (
  326. \is_link($options['verify']) === true
  327. && ($verifyLink = \readlink($options['verify'])) !== false
  328. && \is_dir($verifyLink)
  329. )
  330. ) {
  331. $conf[\CURLOPT_CAPATH] = $options['verify'];
  332. } else {
  333. $conf[\CURLOPT_CAINFO] = $options['verify'];
  334. }
  335. }
  336. }
  337. }
  338. if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) {
  339. $accept = $easy->request->getHeaderLine('Accept-Encoding');
  340. if ($accept) {
  341. $conf[\CURLOPT_ENCODING] = $accept;
  342. } else {
  343. // The empty string enables all available decoders and implicitly
  344. // sets a matching 'Accept-Encoding' header.
  345. $conf[\CURLOPT_ENCODING] = '';
  346. // But as the user did not specify any acceptable encodings we need
  347. // to overwrite this implicit header with an empty one.
  348. $conf[\CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
  349. }
  350. }
  351. if (!isset($options['sink'])) {
  352. // Use a default temp stream if no sink was set.
  353. $options['sink'] = \GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'w+');
  354. }
  355. $sink = $options['sink'];
  356. if (!\is_string($sink)) {
  357. $sink = \GuzzleHttp\Psr7\Utils::streamFor($sink);
  358. } elseif (!\is_dir(\dirname($sink))) {
  359. // Ensure that the directory exists before failing in curl.
  360. throw new \RuntimeException(\sprintf('Directory %s does not exist for sink value of %s', \dirname($sink), $sink));
  361. } else {
  362. $sink = new LazyOpenStream($sink, 'w+');
  363. }
  364. $easy->sink = $sink;
  365. $conf[\CURLOPT_WRITEFUNCTION] = static function ($ch, $write) use ($sink): int {
  366. return $sink->write($write);
  367. };
  368. $timeoutRequiresNoSignal = false;
  369. if (isset($options['timeout'])) {
  370. $timeoutRequiresNoSignal |= $options['timeout'] < 1;
  371. $conf[\CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
  372. }
  373. // CURL default value is CURL_IPRESOLVE_WHATEVER
  374. if (isset($options['force_ip_resolve'])) {
  375. if ('v4' === $options['force_ip_resolve']) {
  376. $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4;
  377. } elseif ('v6' === $options['force_ip_resolve']) {
  378. $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V6;
  379. }
  380. }
  381. if (isset($options['connect_timeout'])) {
  382. $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1;
  383. $conf[\CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
  384. }
  385. if ($timeoutRequiresNoSignal && \strtoupper(\substr(\PHP_OS, 0, 3)) !== 'WIN') {
  386. $conf[\CURLOPT_NOSIGNAL] = true;
  387. }
  388. if (isset($options['proxy'])) {
  389. if (!\is_array($options['proxy'])) {
  390. $conf[\CURLOPT_PROXY] = $options['proxy'];
  391. } else {
  392. $scheme = $easy->request->getUri()->getScheme();
  393. if (isset($options['proxy'][$scheme])) {
  394. $host = $easy->request->getUri()->getHost();
  395. if (isset($options['proxy']['no']) && Utils::isHostInNoProxy($host, $options['proxy']['no'])) {
  396. unset($conf[\CURLOPT_PROXY]);
  397. } else {
  398. $conf[\CURLOPT_PROXY] = $options['proxy'][$scheme];
  399. }
  400. }
  401. }
  402. }
  403. if (isset($options['crypto_method'])) {
  404. if (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']) {
  405. if (!defined('CURL_SSLVERSION_TLSv1_0')) {
  406. throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.0 not supported by your version of cURL');
  407. }
  408. $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_0;
  409. } elseif (\STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method']) {
  410. if (!defined('CURL_SSLVERSION_TLSv1_1')) {
  411. throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.1 not supported by your version of cURL');
  412. }
  413. $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_1;
  414. } elseif (\STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) {
  415. if (!defined('CURL_SSLVERSION_TLSv1_2')) {
  416. throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.2 not supported by your version of cURL');
  417. }
  418. $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2;
  419. } elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) {
  420. if (!defined('CURL_SSLVERSION_TLSv1_3')) {
  421. throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL');
  422. }
  423. $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3;
  424. } else {
  425. throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
  426. }
  427. }
  428. if (isset($options['cert'])) {
  429. $cert = $options['cert'];
  430. if (\is_array($cert)) {
  431. $conf[\CURLOPT_SSLCERTPASSWD] = $cert[1];
  432. $cert = $cert[0];
  433. }
  434. if (!\file_exists($cert)) {
  435. throw new \InvalidArgumentException("SSL certificate not found: {$cert}");
  436. }
  437. // OpenSSL (versions 0.9.3 and later) also support "P12" for PKCS#12-encoded files.
  438. // see https://curl.se/libcurl/c/CURLOPT_SSLCERTTYPE.html
  439. $ext = pathinfo($cert, \PATHINFO_EXTENSION);
  440. if (preg_match('#^(der|p12)$#i', $ext)) {
  441. $conf[\CURLOPT_SSLCERTTYPE] = strtoupper($ext);
  442. }
  443. $conf[\CURLOPT_SSLCERT] = $cert;
  444. }
  445. if (isset($options['ssl_key'])) {
  446. if (\is_array($options['ssl_key'])) {
  447. if (\count($options['ssl_key']) === 2) {
  448. [$sslKey, $conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key'];
  449. } else {
  450. [$sslKey] = $options['ssl_key'];
  451. }
  452. }
  453. $sslKey = $sslKey ?? $options['ssl_key'];
  454. if (!\file_exists($sslKey)) {
  455. throw new \InvalidArgumentException("SSL private key not found: {$sslKey}");
  456. }
  457. $conf[\CURLOPT_SSLKEY] = $sslKey;
  458. }
  459. if (isset($options['progress'])) {
  460. $progress = $options['progress'];
  461. if (!\is_callable($progress)) {
  462. throw new \InvalidArgumentException('progress client option must be callable');
  463. }
  464. $conf[\CURLOPT_NOPROGRESS] = false;
  465. $conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use ($progress) {
  466. $progress($downloadSize, $downloaded, $uploadSize, $uploaded);
  467. };
  468. }
  469. if (!empty($options['debug'])) {
  470. $conf[\CURLOPT_STDERR] = Utils::debugResource($options['debug']);
  471. $conf[\CURLOPT_VERBOSE] = true;
  472. }
  473. }
  474. /**
  475. * This function ensures that a response was set on a transaction. If one
  476. * was not set, then the request is retried if possible. This error
  477. * typically means you are sending a payload, curl encountered a
  478. * "Connection died, retrying a fresh connect" error, tried to rewind the
  479. * stream, and then encountered a "necessary data rewind wasn't possible"
  480. * error, causing the request to be sent through curl_multi_info_read()
  481. * without an error status.
  482. *
  483. * @param callable(RequestInterface, array): PromiseInterface $handler
  484. */
  485. private static function retryFailedRewind(callable $handler, EasyHandle $easy, array $ctx): PromiseInterface
  486. {
  487. try {
  488. // Only rewind if the body has been read from.
  489. $body = $easy->request->getBody();
  490. if ($body->tell() > 0) {
  491. $body->rewind();
  492. }
  493. } catch (\RuntimeException $e) {
  494. $ctx['error'] = 'The connection unexpectedly failed without '
  495. .'providing an error. The request would have been retried, '
  496. .'but attempting to rewind the request body failed. '
  497. .'Exception: '.$e;
  498. return self::createRejection($easy, $ctx);
  499. }
  500. // Retry no more than 3 times before giving up.
  501. if (!isset($easy->options['_curl_retries'])) {
  502. $easy->options['_curl_retries'] = 1;
  503. } elseif ($easy->options['_curl_retries'] == 2) {
  504. $ctx['error'] = 'The cURL request was retried 3 times '
  505. .'and did not succeed. The most likely reason for the failure '
  506. .'is that cURL was unable to rewind the body of the request '
  507. .'and subsequent retries resulted in the same error. Turn on '
  508. .'the debug option to see what went wrong. See '
  509. .'https://bugs.php.net/bug.php?id=47204 for more information.';
  510. return self::createRejection($easy, $ctx);
  511. } else {
  512. ++$easy->options['_curl_retries'];
  513. }
  514. return $handler($easy->request, $easy->options);
  515. }
  516. private function createHeaderFn(EasyHandle $easy): callable
  517. {
  518. if (isset($easy->options['on_headers'])) {
  519. $onHeaders = $easy->options['on_headers'];
  520. if (!\is_callable($onHeaders)) {
  521. throw new \InvalidArgumentException('on_headers must be callable');
  522. }
  523. } else {
  524. $onHeaders = null;
  525. }
  526. return static function ($ch, $h) use (
  527. $onHeaders,
  528. $easy,
  529. &$startingResponse
  530. ) {
  531. $value = \trim($h);
  532. if ($value === '') {
  533. $startingResponse = true;
  534. try {
  535. $easy->createResponse();
  536. } catch (\Exception $e) {
  537. $easy->createResponseException = $e;
  538. return -1;
  539. }
  540. if ($onHeaders !== null) {
  541. try {
  542. $onHeaders($easy->response);
  543. } catch (\Exception $e) {
  544. // Associate the exception with the handle and trigger
  545. // a curl header write error by returning 0.
  546. $easy->onHeadersException = $e;
  547. return -1;
  548. }
  549. }
  550. } elseif ($startingResponse) {
  551. $startingResponse = false;
  552. $easy->headers = [$value];
  553. } else {
  554. $easy->headers[] = $value;
  555. }
  556. return \strlen($h);
  557. };
  558. }
  559. public function __destruct()
  560. {
  561. foreach ($this->handles as $id => $handle) {
  562. \curl_close($handle);
  563. unset($this->handles[$id]);
  564. }
  565. }
  566. }