CurlClient.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730
  1. <?php
  2. namespace Stripe\HttpClient;
  3. use Stripe\Exception;
  4. use Stripe\Stripe;
  5. use Stripe\Util;
  6. // @codingStandardsIgnoreStart
  7. // PSR2 requires all constants be upper case. Sadly, the CURL_SSLVERSION
  8. // constants do not abide by those rules.
  9. // Note the values come from their position in the enums that
  10. // defines them in cURL's source code.
  11. // Available since PHP 5.5.19 and 5.6.3
  12. if (!\defined('CURL_SSLVERSION_TLSv1_2')) {
  13. \define('CURL_SSLVERSION_TLSv1_2', 6);
  14. }
  15. // @codingStandardsIgnoreEnd
  16. // Available since PHP 7.0.7 and cURL 7.47.0
  17. if (!\defined('CURL_HTTP_VERSION_2TLS')) {
  18. \define('CURL_HTTP_VERSION_2TLS', 4);
  19. }
  20. class CurlClient implements ClientInterface, StreamingClientInterface
  21. {
  22. protected static $instance;
  23. public static function instance()
  24. {
  25. if (!static::$instance) {
  26. static::$instance = new static();
  27. }
  28. return static::$instance;
  29. }
  30. protected $defaultOptions;
  31. /** @var \Stripe\Util\RandomGenerator */
  32. protected $randomGenerator;
  33. protected $userAgentInfo;
  34. protected $enablePersistentConnections = true;
  35. protected $enableHttp2;
  36. protected $curlHandle;
  37. protected $requestStatusCallback;
  38. /**
  39. * CurlClient constructor.
  40. *
  41. * Pass in a callable to $defaultOptions that returns an array of CURLOPT_* values to start
  42. * off a request with, or an flat array with the same format used by curl_setopt_array() to
  43. * provide a static set of options. Note that many options are overridden later in the request
  44. * call, including timeouts, which can be set via setTimeout() and setConnectTimeout().
  45. *
  46. * Note that request() will silently ignore a non-callable, non-array $defaultOptions, and will
  47. * throw an exception if $defaultOptions returns a non-array value.
  48. *
  49. * @param null|array|callable $defaultOptions
  50. * @param null|\Stripe\Util\RandomGenerator $randomGenerator
  51. */
  52. public function __construct($defaultOptions = null, $randomGenerator = null)
  53. {
  54. $this->defaultOptions = $defaultOptions;
  55. $this->randomGenerator = $randomGenerator ?: new Util\RandomGenerator();
  56. $this->initUserAgentInfo();
  57. $this->enableHttp2 = $this->canSafelyUseHttp2();
  58. }
  59. public function __destruct()
  60. {
  61. $this->closeCurlHandle();
  62. }
  63. public function initUserAgentInfo()
  64. {
  65. $curlVersion = \curl_version();
  66. $this->userAgentInfo = [
  67. 'httplib' => 'curl ' . $curlVersion['version'],
  68. 'ssllib' => $curlVersion['ssl_version'],
  69. ];
  70. }
  71. public function getDefaultOptions()
  72. {
  73. return $this->defaultOptions;
  74. }
  75. public function getUserAgentInfo()
  76. {
  77. return $this->userAgentInfo;
  78. }
  79. /**
  80. * @return bool
  81. */
  82. public function getEnablePersistentConnections()
  83. {
  84. return $this->enablePersistentConnections;
  85. }
  86. /**
  87. * @param bool $enable
  88. */
  89. public function setEnablePersistentConnections($enable)
  90. {
  91. $this->enablePersistentConnections = $enable;
  92. }
  93. /**
  94. * @return bool
  95. */
  96. public function getEnableHttp2()
  97. {
  98. return $this->enableHttp2;
  99. }
  100. /**
  101. * @param bool $enable
  102. */
  103. public function setEnableHttp2($enable)
  104. {
  105. $this->enableHttp2 = $enable;
  106. }
  107. /**
  108. * @return null|callable
  109. */
  110. public function getRequestStatusCallback()
  111. {
  112. return $this->requestStatusCallback;
  113. }
  114. /**
  115. * Sets a callback that is called after each request. The callback will
  116. * receive the following parameters:
  117. * <ol>
  118. * <li>string $rbody The response body</li>
  119. * <li>integer $rcode The response status code</li>
  120. * <li>\Stripe\Util\CaseInsensitiveArray $rheaders The response headers</li>
  121. * <li>integer $errno The curl error number</li>
  122. * <li>string|null $message The curl error message</li>
  123. * <li>boolean $shouldRetry Whether the request will be retried</li>
  124. * <li>integer $numRetries The number of the retry attempt</li>
  125. * </ol>.
  126. *
  127. * @param null|callable $requestStatusCallback
  128. */
  129. public function setRequestStatusCallback($requestStatusCallback)
  130. {
  131. $this->requestStatusCallback = $requestStatusCallback;
  132. }
  133. // USER DEFINED TIMEOUTS
  134. const DEFAULT_TIMEOUT = 80;
  135. const DEFAULT_CONNECT_TIMEOUT = 30;
  136. private $timeout = self::DEFAULT_TIMEOUT;
  137. private $connectTimeout = self::DEFAULT_CONNECT_TIMEOUT;
  138. public function setTimeout($seconds)
  139. {
  140. $this->timeout = (int) \max($seconds, 0);
  141. return $this;
  142. }
  143. public function setConnectTimeout($seconds)
  144. {
  145. $this->connectTimeout = (int) \max($seconds, 0);
  146. return $this;
  147. }
  148. public function getTimeout()
  149. {
  150. return $this->timeout;
  151. }
  152. public function getConnectTimeout()
  153. {
  154. return $this->connectTimeout;
  155. }
  156. // END OF USER DEFINED TIMEOUTS
  157. private function constructRequest($method, $absUrl, $headers, $params, $hasFile)
  158. {
  159. $method = \strtolower($method);
  160. $opts = [];
  161. if (\is_callable($this->defaultOptions)) { // call defaultOptions callback, set options to return value
  162. $opts = \call_user_func_array($this->defaultOptions, \func_get_args());
  163. if (!\is_array($opts)) {
  164. throw new Exception\UnexpectedValueException('Non-array value returned by defaultOptions CurlClient callback');
  165. }
  166. } elseif (\is_array($this->defaultOptions)) { // set default curlopts from array
  167. $opts = $this->defaultOptions;
  168. }
  169. $params = Util\Util::objectsToIds($params);
  170. if ('get' === $method) {
  171. if ($hasFile) {
  172. throw new Exception\UnexpectedValueException(
  173. 'Issuing a GET request with a file parameter'
  174. );
  175. }
  176. $opts[\CURLOPT_HTTPGET] = 1;
  177. if (\count($params) > 0) {
  178. $encoded = Util\Util::encodeParameters($params);
  179. $absUrl = "{$absUrl}?{$encoded}";
  180. }
  181. } elseif ('post' === $method) {
  182. $opts[\CURLOPT_POST] = 1;
  183. $opts[\CURLOPT_POSTFIELDS] = $hasFile ? $params : Util\Util::encodeParameters($params);
  184. } elseif ('delete' === $method) {
  185. $opts[\CURLOPT_CUSTOMREQUEST] = 'DELETE';
  186. if (\count($params) > 0) {
  187. $encoded = Util\Util::encodeParameters($params);
  188. $absUrl = "{$absUrl}?{$encoded}";
  189. }
  190. } else {
  191. throw new Exception\UnexpectedValueException("Unrecognized method {$method}");
  192. }
  193. // It is only safe to retry network failures on POST requests if we
  194. // add an Idempotency-Key header
  195. if (('post' === $method) && (Stripe::$maxNetworkRetries > 0)) {
  196. if (!$this->hasHeader($headers, 'Idempotency-Key')) {
  197. $headers[] = 'Idempotency-Key: ' . $this->randomGenerator->uuid();
  198. }
  199. }
  200. // By default for large request body sizes (> 1024 bytes), cURL will
  201. // send a request without a body and with a `Expect: 100-continue`
  202. // header, which gives the server a chance to respond with an error
  203. // status code in cases where one can be determined right away (say
  204. // on an authentication problem for example), and saves the "large"
  205. // request body from being ever sent.
  206. //
  207. // Unfortunately, the bindings don't currently correctly handle the
  208. // success case (in which the server sends back a 100 CONTINUE), so
  209. // we'll error under that condition. To compensate for that problem
  210. // for the time being, override cURL's behavior by simply always
  211. // sending an empty `Expect:` header.
  212. $headers[] = 'Expect: ';
  213. $absUrl = Util\Util::utf8($absUrl);
  214. $opts[\CURLOPT_URL] = $absUrl;
  215. $opts[\CURLOPT_RETURNTRANSFER] = true;
  216. $opts[\CURLOPT_CONNECTTIMEOUT] = $this->connectTimeout;
  217. $opts[\CURLOPT_TIMEOUT] = $this->timeout;
  218. $opts[\CURLOPT_HTTPHEADER] = $headers;
  219. $opts[\CURLOPT_CAINFO] = Stripe::getCABundlePath();
  220. if (!Stripe::getVerifySslCerts()) {
  221. $opts[\CURLOPT_SSL_VERIFYPEER] = false;
  222. }
  223. if (!isset($opts[\CURLOPT_HTTP_VERSION]) && $this->getEnableHttp2()) {
  224. // For HTTPS requests, enable HTTP/2, if supported
  225. $opts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2TLS;
  226. }
  227. return [$opts, $absUrl];
  228. }
  229. public function request($method, $absUrl, $headers, $params, $hasFile)
  230. {
  231. list($opts, $absUrl) = $this->constructRequest($method, $absUrl, $headers, $params, $hasFile);
  232. list($rbody, $rcode, $rheaders) = $this->executeRequestWithRetries($opts, $absUrl);
  233. return [$rbody, $rcode, $rheaders];
  234. }
  235. public function requestStream($method, $absUrl, $headers, $params, $hasFile, $readBodyChunk)
  236. {
  237. list($opts, $absUrl) = $this->constructRequest($method, $absUrl, $headers, $params, $hasFile);
  238. $opts[\CURLOPT_RETURNTRANSFER] = false;
  239. list($rbody, $rcode, $rheaders) = $this->executeStreamingRequestWithRetries($opts, $absUrl, $readBodyChunk);
  240. return [$rbody, $rcode, $rheaders];
  241. }
  242. /**
  243. * Curl permits sending \CURLOPT_HEADERFUNCTION, which is called with lines
  244. * from the header and \CURLOPT_WRITEFUNCTION, which is called with bytes
  245. * from the body. You usually want to handle the body differently depending
  246. * on what was in the header.
  247. *
  248. * This function makes it easier to specify different callbacks depending
  249. * on the contents of the heeder. After the header has been completely read
  250. * and the body begins to stream, it will call $determineWriteCallback with
  251. * the array of headers. $determineWriteCallback should, based on the
  252. * headers it receives, return a "writeCallback" that describes what to do
  253. * with the incoming HTTP response body.
  254. *
  255. * @param array $opts
  256. * @param callable $determineWriteCallback
  257. *
  258. * @return array
  259. */
  260. private function useHeadersToDetermineWriteCallback($opts, $determineWriteCallback)
  261. {
  262. $rheaders = new Util\CaseInsensitiveArray();
  263. $headerCallback = function ($curl, $header_line) use (&$rheaders) {
  264. return self::parseLineIntoHeaderArray($header_line, $rheaders);
  265. };
  266. $writeCallback = null;
  267. $writeCallbackWrapper = function ($curl, $data) use (&$writeCallback, &$rheaders, &$determineWriteCallback) {
  268. if (null === $writeCallback) {
  269. $writeCallback = \call_user_func_array($determineWriteCallback, [$rheaders]);
  270. }
  271. return \call_user_func_array($writeCallback, [$curl, $data]);
  272. };
  273. return [$headerCallback, $writeCallbackWrapper];
  274. }
  275. private static function parseLineIntoHeaderArray($line, &$headers)
  276. {
  277. if (false === \strpos($line, ':')) {
  278. return \strlen($line);
  279. }
  280. list($key, $value) = \explode(':', \trim($line), 2);
  281. $headers[\trim($key)] = \trim($value);
  282. return \strlen($line);
  283. }
  284. /**
  285. * Like `executeRequestWithRetries` except:
  286. * 1. Does not buffer the body of a successful (status code < 300)
  287. * response into memory -- instead, calls the caller-provided
  288. * $readBodyChunk with each chunk of incoming data.
  289. * 2. Does not retry if a network error occurs while streaming the
  290. * body of a successful response.
  291. *
  292. * @param array $opts cURL options
  293. * @param string $absUrl
  294. * @param callable $readBodyChunk
  295. *
  296. * @return array
  297. */
  298. public function executeStreamingRequestWithRetries($opts, $absUrl, $readBodyChunk)
  299. {
  300. /** @var bool */
  301. $shouldRetry = false;
  302. /** @var int */
  303. $numRetries = 0;
  304. // Will contain the bytes of the body of the last request
  305. // if it was not successful and should not be retries
  306. /** @var null|string */
  307. $rbody = null;
  308. // Status code of the last request
  309. /** @var null|bool */
  310. $rcode = null;
  311. // Array of headers from the last request
  312. /** @var null|array */
  313. $lastRHeaders = null;
  314. $errno = null;
  315. $message = null;
  316. $determineWriteCallback = function ($rheaders) use (
  317. &$readBodyChunk,
  318. &$shouldRetry,
  319. &$rbody,
  320. &$numRetries,
  321. &$rcode,
  322. &$lastRHeaders,
  323. &$errno
  324. ) {
  325. $lastRHeaders = $rheaders;
  326. $errno = \curl_errno($this->curlHandle);
  327. $rcode = \curl_getinfo($this->curlHandle, \CURLINFO_HTTP_CODE);
  328. // Send the bytes from the body of a successful request to the caller-provided $readBodyChunk.
  329. if ($rcode < 300) {
  330. $rbody = null;
  331. return function ($curl, $data) use (&$readBodyChunk) {
  332. // Don't expose the $curl handle to the user, and don't require them to
  333. // return the length of $data.
  334. \call_user_func_array($readBodyChunk, [$data]);
  335. return \strlen($data);
  336. };
  337. }
  338. $shouldRetry = $this->shouldRetry($errno, $rcode, $rheaders, $numRetries);
  339. // Discard the body from an unsuccessful request that should be retried.
  340. if ($shouldRetry) {
  341. return function ($curl, $data) {
  342. return \strlen($data);
  343. };
  344. } else {
  345. // Otherwise, buffer the body into $rbody. It will need to be parsed to determine
  346. // which exception to throw to the user.
  347. $rbody = '';
  348. return function ($curl, $data) use (&$rbody) {
  349. $rbody .= $data;
  350. return \strlen($data);
  351. };
  352. }
  353. };
  354. while (true) {
  355. list($headerCallback, $writeCallback) = $this->useHeadersToDetermineWriteCallback($opts, $determineWriteCallback);
  356. $opts[\CURLOPT_HEADERFUNCTION] = $headerCallback;
  357. $opts[\CURLOPT_WRITEFUNCTION] = $writeCallback;
  358. $shouldRetry = false;
  359. $rbody = null;
  360. $this->resetCurlHandle();
  361. \curl_setopt_array($this->curlHandle, $opts);
  362. $result = \curl_exec($this->curlHandle);
  363. $errno = \curl_errno($this->curlHandle);
  364. if (0 !== $errno) {
  365. $message = \curl_error($this->curlHandle);
  366. }
  367. if (!$this->getEnablePersistentConnections()) {
  368. $this->closeCurlHandle();
  369. }
  370. if (\is_callable($this->getRequestStatusCallback())) {
  371. \call_user_func_array(
  372. $this->getRequestStatusCallback(),
  373. [$rbody, $rcode, $lastRHeaders, $errno, $message, $shouldRetry, $numRetries]
  374. );
  375. }
  376. if ($shouldRetry) {
  377. ++$numRetries;
  378. $sleepSeconds = $this->sleepTime($numRetries, $lastRHeaders);
  379. \usleep((int) ($sleepSeconds * 1000000));
  380. } else {
  381. break;
  382. }
  383. }
  384. if (0 !== $errno) {
  385. $this->handleCurlError($absUrl, $errno, $message, $numRetries);
  386. }
  387. return [$rbody, $rcode, $lastRHeaders];
  388. }
  389. /**
  390. * @param array $opts cURL options
  391. * @param string $absUrl
  392. */
  393. public function executeRequestWithRetries($opts, $absUrl)
  394. {
  395. $numRetries = 0;
  396. while (true) {
  397. $rcode = 0;
  398. $errno = 0;
  399. $message = null;
  400. // Create a callback to capture HTTP headers for the response
  401. $rheaders = new Util\CaseInsensitiveArray();
  402. $headerCallback = function ($curl, $header_line) use (&$rheaders) {
  403. return CurlClient::parseLineIntoHeaderArray($header_line, $rheaders);
  404. };
  405. $opts[\CURLOPT_HEADERFUNCTION] = $headerCallback;
  406. $this->resetCurlHandle();
  407. \curl_setopt_array($this->curlHandle, $opts);
  408. $rbody = \curl_exec($this->curlHandle);
  409. if (false === $rbody) {
  410. $errno = \curl_errno($this->curlHandle);
  411. $message = \curl_error($this->curlHandle);
  412. } else {
  413. $rcode = \curl_getinfo($this->curlHandle, \CURLINFO_HTTP_CODE);
  414. }
  415. if (!$this->getEnablePersistentConnections()) {
  416. $this->closeCurlHandle();
  417. }
  418. $shouldRetry = $this->shouldRetry($errno, $rcode, $rheaders, $numRetries);
  419. if (\is_callable($this->getRequestStatusCallback())) {
  420. \call_user_func_array(
  421. $this->getRequestStatusCallback(),
  422. [$rbody, $rcode, $rheaders, $errno, $message, $shouldRetry, $numRetries]
  423. );
  424. }
  425. if ($shouldRetry) {
  426. ++$numRetries;
  427. $sleepSeconds = $this->sleepTime($numRetries, $rheaders);
  428. \usleep((int) ($sleepSeconds * 1000000));
  429. } else {
  430. break;
  431. }
  432. }
  433. if (false === $rbody) {
  434. $this->handleCurlError($absUrl, $errno, $message, $numRetries);
  435. }
  436. return [$rbody, $rcode, $rheaders];
  437. }
  438. /**
  439. * @param string $url
  440. * @param int $errno
  441. * @param string $message
  442. * @param int $numRetries
  443. *
  444. * @throws Exception\ApiConnectionException
  445. */
  446. private function handleCurlError($url, $errno, $message, $numRetries)
  447. {
  448. switch ($errno) {
  449. case \CURLE_COULDNT_CONNECT:
  450. case \CURLE_COULDNT_RESOLVE_HOST:
  451. case \CURLE_OPERATION_TIMEOUTED:
  452. $msg = "Could not connect to Stripe ({$url}). Please check your "
  453. . 'internet connection and try again. If this problem persists, '
  454. . "you should check Stripe's service status at "
  455. . 'https://twitter.com/stripestatus, or';
  456. break;
  457. case \CURLE_SSL_CACERT:
  458. case \CURLE_SSL_PEER_CERTIFICATE:
  459. $msg = "Could not verify Stripe's SSL certificate. Please make sure "
  460. . 'that your network is not intercepting certificates. '
  461. . "(Try going to {$url} in your browser.) "
  462. . 'If this problem persists,';
  463. break;
  464. default:
  465. $msg = 'Unexpected error communicating with Stripe. '
  466. . 'If this problem persists,';
  467. }
  468. $msg .= ' let us know at support@stripe.com.';
  469. $msg .= "\n\n(Network error [errno {$errno}]: {$message})";
  470. if ($numRetries > 0) {
  471. $msg .= "\n\nRequest was retried {$numRetries} times.";
  472. }
  473. throw new Exception\ApiConnectionException($msg);
  474. }
  475. /**
  476. * Checks if an error is a problem that we should retry on. This includes both
  477. * socket errors that may represent an intermittent problem and some special
  478. * HTTP statuses.
  479. *
  480. * @param int $errno
  481. * @param int $rcode
  482. * @param array|\Stripe\Util\CaseInsensitiveArray $rheaders
  483. * @param int $numRetries
  484. *
  485. * @return bool
  486. */
  487. private function shouldRetry($errno, $rcode, $rheaders, $numRetries)
  488. {
  489. if ($numRetries >= Stripe::getMaxNetworkRetries()) {
  490. return false;
  491. }
  492. // Retry on timeout-related problems (either on open or read).
  493. if (\CURLE_OPERATION_TIMEOUTED === $errno) {
  494. return true;
  495. }
  496. // Destination refused the connection, the connection was reset, or a
  497. // variety of other connection failures. This could occur from a single
  498. // saturated server, so retry in case it's intermittent.
  499. if (\CURLE_COULDNT_CONNECT === $errno) {
  500. return true;
  501. }
  502. // The API may ask us not to retry (eg; if doing so would be a no-op)
  503. // or advise us to retry (eg; in cases of lock timeouts); we defer to that.
  504. if (isset($rheaders['stripe-should-retry'])) {
  505. if ('false' === $rheaders['stripe-should-retry']) {
  506. return false;
  507. }
  508. if ('true' === $rheaders['stripe-should-retry']) {
  509. return true;
  510. }
  511. }
  512. // 409 Conflict
  513. if (409 === $rcode) {
  514. return true;
  515. }
  516. // Retry on 500, 503, and other internal errors.
  517. //
  518. // Note that we expect the stripe-should-retry header to be false
  519. // in most cases when a 500 is returned, since our idempotency framework
  520. // would typically replay it anyway.
  521. if ($rcode >= 500) {
  522. return true;
  523. }
  524. return false;
  525. }
  526. /**
  527. * Provides the number of seconds to wait before retrying a request.
  528. *
  529. * @param int $numRetries
  530. * @param array|\Stripe\Util\CaseInsensitiveArray $rheaders
  531. *
  532. * @return int
  533. */
  534. private function sleepTime($numRetries, $rheaders)
  535. {
  536. // Apply exponential backoff with $initialNetworkRetryDelay on the
  537. // number of $numRetries so far as inputs. Do not allow the number to exceed
  538. // $maxNetworkRetryDelay.
  539. $sleepSeconds = \min(
  540. Stripe::getInitialNetworkRetryDelay() * 1.0 * 2 ** ($numRetries - 1),
  541. Stripe::getMaxNetworkRetryDelay()
  542. );
  543. // Apply some jitter by randomizing the value in the range of
  544. // ($sleepSeconds / 2) to ($sleepSeconds).
  545. $sleepSeconds *= 0.5 * (1 + $this->randomGenerator->randFloat());
  546. // But never sleep less than the base sleep seconds.
  547. $sleepSeconds = \max(Stripe::getInitialNetworkRetryDelay(), $sleepSeconds);
  548. // And never sleep less than the time the API asks us to wait, assuming it's a reasonable ask.
  549. $retryAfter = isset($rheaders['retry-after']) ? (float) ($rheaders['retry-after']) : 0.0;
  550. if (\floor($retryAfter) === $retryAfter && $retryAfter <= Stripe::getMaxRetryAfter()) {
  551. $sleepSeconds = \max($sleepSeconds, $retryAfter);
  552. }
  553. return $sleepSeconds;
  554. }
  555. /**
  556. * Initializes the curl handle. If already initialized, the handle is closed first.
  557. */
  558. private function initCurlHandle()
  559. {
  560. $this->closeCurlHandle();
  561. $this->curlHandle = \curl_init();
  562. }
  563. /**
  564. * Closes the curl handle if initialized. Do nothing if already closed.
  565. */
  566. private function closeCurlHandle()
  567. {
  568. if (null !== $this->curlHandle) {
  569. \curl_close($this->curlHandle);
  570. $this->curlHandle = null;
  571. }
  572. }
  573. /**
  574. * Resets the curl handle. If the handle is not already initialized, or if persistent
  575. * connections are disabled, the handle is reinitialized instead.
  576. */
  577. private function resetCurlHandle()
  578. {
  579. if (null !== $this->curlHandle && $this->getEnablePersistentConnections()) {
  580. \curl_reset($this->curlHandle);
  581. } else {
  582. $this->initCurlHandle();
  583. }
  584. }
  585. /**
  586. * Indicates whether it is safe to use HTTP/2 or not.
  587. *
  588. * @return bool
  589. */
  590. private function canSafelyUseHttp2()
  591. {
  592. // Versions of curl older than 7.60.0 don't respect GOAWAY frames
  593. // (cf. https://github.com/curl/curl/issues/2416), which Stripe use.
  594. $curlVersion = \curl_version()['version'];
  595. return \version_compare($curlVersion, '7.60.0') >= 0;
  596. }
  597. /**
  598. * Checks if a list of headers contains a specific header name.
  599. *
  600. * @param string[] $headers
  601. * @param string $name
  602. *
  603. * @return bool
  604. */
  605. private function hasHeader($headers, $name)
  606. {
  607. foreach ($headers as $header) {
  608. if (0 === \strncasecmp($header, "{$name}: ", \strlen($name) + 2)) {
  609. return true;
  610. }
  611. }
  612. return false;
  613. }
  614. }