CurlClient.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734
  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. // Stripe's API servers are only accessible over IPv4. Force IPv4 resolving to avoid
  228. // potential issues (cf. https://github.com/stripe/stripe-php/issues/1045).
  229. $opts[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4;
  230. return [$opts, $absUrl];
  231. }
  232. public function request($method, $absUrl, $headers, $params, $hasFile)
  233. {
  234. list($opts, $absUrl) = $this->constructRequest($method, $absUrl, $headers, $params, $hasFile);
  235. list($rbody, $rcode, $rheaders) = $this->executeRequestWithRetries($opts, $absUrl);
  236. return [$rbody, $rcode, $rheaders];
  237. }
  238. public function requestStream($method, $absUrl, $headers, $params, $hasFile, $readBodyChunk)
  239. {
  240. list($opts, $absUrl) = $this->constructRequest($method, $absUrl, $headers, $params, $hasFile);
  241. $opts[\CURLOPT_RETURNTRANSFER] = false;
  242. list($rbody, $rcode, $rheaders) = $this->executeStreamingRequestWithRetries($opts, $absUrl, $readBodyChunk);
  243. return [$rbody, $rcode, $rheaders];
  244. }
  245. /**
  246. * Curl permits sending \CURLOPT_HEADERFUNCTION, which is called with lines
  247. * from the header and \CURLOPT_WRITEFUNCTION, which is called with bytes
  248. * from the body. You usually want to handle the body differently depending
  249. * on what was in the header.
  250. *
  251. * This function makes it easier to specify different callbacks depending
  252. * on the contents of the heeder. After the header has been completely read
  253. * and the body begins to stream, it will call $determineWriteCallback with
  254. * the array of headers. $determineWriteCallback should, based on the
  255. * headers it receives, return a "writeCallback" that describes what to do
  256. * with the incoming HTTP response body.
  257. *
  258. * @param array $opts
  259. * @param callable $determineWriteCallback
  260. *
  261. * @return array
  262. */
  263. private function useHeadersToDetermineWriteCallback($opts, $determineWriteCallback)
  264. {
  265. $rheaders = new Util\CaseInsensitiveArray();
  266. $headerCallback = function ($curl, $header_line) use (&$rheaders) {
  267. return self::parseLineIntoHeaderArray($header_line, $rheaders);
  268. };
  269. $writeCallback = null;
  270. $writeCallbackWrapper = function ($curl, $data) use (&$writeCallback, &$rheaders, &$determineWriteCallback) {
  271. if (null === $writeCallback) {
  272. $writeCallback = \call_user_func_array($determineWriteCallback, [$rheaders]);
  273. }
  274. return \call_user_func_array($writeCallback, [$curl, $data]);
  275. };
  276. return [$headerCallback, $writeCallbackWrapper];
  277. }
  278. private static function parseLineIntoHeaderArray($line, &$headers)
  279. {
  280. if (false === \strpos($line, ':')) {
  281. return \strlen($line);
  282. }
  283. list($key, $value) = \explode(':', \trim($line), 2);
  284. $headers[\trim($key)] = \trim($value);
  285. return \strlen($line);
  286. }
  287. /**
  288. * Like `executeRequestWithRetries` except:
  289. * 1. Does not buffer the body of a successful (status code < 300)
  290. * response into memory -- instead, calls the caller-provided
  291. * $readBodyChunk with each chunk of incoming data.
  292. * 2. Does not retry if a network error occurs while streaming the
  293. * body of a successful response.
  294. *
  295. * @param array $opts cURL options
  296. * @param string $absUrl
  297. * @param callable $readBodyChunk
  298. *
  299. * @return array
  300. */
  301. public function executeStreamingRequestWithRetries($opts, $absUrl, $readBodyChunk)
  302. {
  303. /** @var bool */
  304. $shouldRetry = false;
  305. /** @var int */
  306. $numRetries = 0;
  307. // Will contain the bytes of the body of the last request
  308. // if it was not successful and should not be retries
  309. /** @var null|string */
  310. $rbody = null;
  311. // Status code of the last request
  312. /** @var null|bool */
  313. $rcode = null;
  314. // Array of headers from the last request
  315. /** @var null|array */
  316. $lastRHeaders = null;
  317. $errno = null;
  318. $message = null;
  319. $determineWriteCallback = function ($rheaders) use (
  320. &$readBodyChunk,
  321. &$shouldRetry,
  322. &$rbody,
  323. &$numRetries,
  324. &$rcode,
  325. &$lastRHeaders,
  326. &$errno
  327. ) {
  328. $lastRHeaders = $rheaders;
  329. $errno = \curl_errno($this->curlHandle);
  330. $rcode = \curl_getinfo($this->curlHandle, \CURLINFO_HTTP_CODE);
  331. // Send the bytes from the body of a successful request to the caller-provided $readBodyChunk.
  332. if ($rcode < 300) {
  333. $rbody = null;
  334. return function ($curl, $data) use (&$readBodyChunk) {
  335. // Don't expose the $curl handle to the user, and don't require them to
  336. // return the length of $data.
  337. \call_user_func_array($readBodyChunk, [$data]);
  338. return \strlen($data);
  339. };
  340. }
  341. $shouldRetry = $this->shouldRetry($errno, $rcode, $rheaders, $numRetries);
  342. // Discard the body from an unsuccessful request that should be retried.
  343. if ($shouldRetry) {
  344. return function ($curl, $data) {
  345. return \strlen($data);
  346. };
  347. } else {
  348. // Otherwise, buffer the body into $rbody. It will need to be parsed to determine
  349. // which exception to throw to the user.
  350. $rbody = '';
  351. return function ($curl, $data) use (&$rbody) {
  352. $rbody .= $data;
  353. return \strlen($data);
  354. };
  355. }
  356. };
  357. while (true) {
  358. list($headerCallback, $writeCallback) = $this->useHeadersToDetermineWriteCallback($opts, $determineWriteCallback);
  359. $opts[\CURLOPT_HEADERFUNCTION] = $headerCallback;
  360. $opts[\CURLOPT_WRITEFUNCTION] = $writeCallback;
  361. $shouldRetry = false;
  362. $rbody = null;
  363. $this->resetCurlHandle();
  364. \curl_setopt_array($this->curlHandle, $opts);
  365. $result = \curl_exec($this->curlHandle);
  366. $errno = \curl_errno($this->curlHandle);
  367. if (0 !== $errno) {
  368. $message = \curl_error($this->curlHandle);
  369. }
  370. if (!$this->getEnablePersistentConnections()) {
  371. $this->closeCurlHandle();
  372. }
  373. if (\is_callable($this->getRequestStatusCallback())) {
  374. \call_user_func_array(
  375. $this->getRequestStatusCallback(),
  376. [$rbody, $rcode, $lastRHeaders, $errno, $message, $shouldRetry, $numRetries]
  377. );
  378. }
  379. if ($shouldRetry) {
  380. ++$numRetries;
  381. $sleepSeconds = $this->sleepTime($numRetries, $lastRHeaders);
  382. \usleep((int) ($sleepSeconds * 1000000));
  383. } else {
  384. break;
  385. }
  386. }
  387. if (0 !== $errno) {
  388. $this->handleCurlError($absUrl, $errno, $message, $numRetries);
  389. }
  390. return [$rbody, $rcode, $lastRHeaders];
  391. }
  392. /**
  393. * @param array $opts cURL options
  394. * @param string $absUrl
  395. */
  396. public function executeRequestWithRetries($opts, $absUrl)
  397. {
  398. $numRetries = 0;
  399. while (true) {
  400. $rcode = 0;
  401. $errno = 0;
  402. $message = null;
  403. // Create a callback to capture HTTP headers for the response
  404. $rheaders = new Util\CaseInsensitiveArray();
  405. $headerCallback = function ($curl, $header_line) use (&$rheaders) {
  406. return CurlClient::parseLineIntoHeaderArray($header_line, $rheaders);
  407. };
  408. $opts[\CURLOPT_HEADERFUNCTION] = $headerCallback;
  409. $this->resetCurlHandle();
  410. \curl_setopt_array($this->curlHandle, $opts);
  411. $rbody = \curl_exec($this->curlHandle);
  412. if (false === $rbody) {
  413. $errno = \curl_errno($this->curlHandle);
  414. $message = \curl_error($this->curlHandle);
  415. } else {
  416. $rcode = \curl_getinfo($this->curlHandle, \CURLINFO_HTTP_CODE);
  417. }
  418. if (!$this->getEnablePersistentConnections()) {
  419. $this->closeCurlHandle();
  420. }
  421. $shouldRetry = $this->shouldRetry($errno, $rcode, $rheaders, $numRetries);
  422. if (\is_callable($this->getRequestStatusCallback())) {
  423. \call_user_func_array(
  424. $this->getRequestStatusCallback(),
  425. [$rbody, $rcode, $rheaders, $errno, $message, $shouldRetry, $numRetries]
  426. );
  427. }
  428. if ($shouldRetry) {
  429. ++$numRetries;
  430. $sleepSeconds = $this->sleepTime($numRetries, $rheaders);
  431. \usleep((int) ($sleepSeconds * 1000000));
  432. } else {
  433. break;
  434. }
  435. }
  436. if (false === $rbody) {
  437. $this->handleCurlError($absUrl, $errno, $message, $numRetries);
  438. }
  439. return [$rbody, $rcode, $rheaders];
  440. }
  441. /**
  442. * @param string $url
  443. * @param int $errno
  444. * @param string $message
  445. * @param int $numRetries
  446. *
  447. * @throws Exception\ApiConnectionException
  448. */
  449. private function handleCurlError($url, $errno, $message, $numRetries)
  450. {
  451. switch ($errno) {
  452. case \CURLE_COULDNT_CONNECT:
  453. case \CURLE_COULDNT_RESOLVE_HOST:
  454. case \CURLE_OPERATION_TIMEOUTED:
  455. $msg = "Could not connect to Stripe ({$url}). Please check your "
  456. . 'internet connection and try again. If this problem persists, '
  457. . "you should check Stripe's service status at "
  458. . 'https://twitter.com/stripestatus, or';
  459. break;
  460. case \CURLE_SSL_CACERT:
  461. case \CURLE_SSL_PEER_CERTIFICATE:
  462. $msg = "Could not verify Stripe's SSL certificate. Please make sure "
  463. . 'that your network is not intercepting certificates. '
  464. . "(Try going to {$url} in your browser.) "
  465. . 'If this problem persists,';
  466. break;
  467. default:
  468. $msg = 'Unexpected error communicating with Stripe. '
  469. . 'If this problem persists,';
  470. }
  471. $msg .= ' let us know at support@stripe.com.';
  472. $msg .= "\n\n(Network error [errno {$errno}]: {$message})";
  473. if ($numRetries > 0) {
  474. $msg .= "\n\nRequest was retried {$numRetries} times.";
  475. }
  476. throw new Exception\ApiConnectionException($msg);
  477. }
  478. /**
  479. * Checks if an error is a problem that we should retry on. This includes both
  480. * socket errors that may represent an intermittent problem and some special
  481. * HTTP statuses.
  482. *
  483. * @param int $errno
  484. * @param int $rcode
  485. * @param array|\Stripe\Util\CaseInsensitiveArray $rheaders
  486. * @param int $numRetries
  487. *
  488. * @return bool
  489. */
  490. private function shouldRetry($errno, $rcode, $rheaders, $numRetries)
  491. {
  492. if ($numRetries >= Stripe::getMaxNetworkRetries()) {
  493. return false;
  494. }
  495. // Retry on timeout-related problems (either on open or read).
  496. if (\CURLE_OPERATION_TIMEOUTED === $errno) {
  497. return true;
  498. }
  499. // Destination refused the connection, the connection was reset, or a
  500. // variety of other connection failures. This could occur from a single
  501. // saturated server, so retry in case it's intermittent.
  502. if (\CURLE_COULDNT_CONNECT === $errno) {
  503. return true;
  504. }
  505. // The API may ask us not to retry (eg; if doing so would be a no-op)
  506. // or advise us to retry (eg; in cases of lock timeouts); we defer to that.
  507. if (isset($rheaders['stripe-should-retry'])) {
  508. if ('false' === $rheaders['stripe-should-retry']) {
  509. return false;
  510. }
  511. if ('true' === $rheaders['stripe-should-retry']) {
  512. return true;
  513. }
  514. }
  515. // 409 Conflict
  516. if (409 === $rcode) {
  517. return true;
  518. }
  519. // Retry on 500, 503, and other internal errors.
  520. //
  521. // Note that we expect the stripe-should-retry header to be false
  522. // in most cases when a 500 is returned, since our idempotency framework
  523. // would typically replay it anyway.
  524. if ($rcode >= 500) {
  525. return true;
  526. }
  527. return false;
  528. }
  529. /**
  530. * Provides the number of seconds to wait before retrying a request.
  531. *
  532. * @param int $numRetries
  533. * @param array|\Stripe\Util\CaseInsensitiveArray $rheaders
  534. *
  535. * @return int
  536. */
  537. private function sleepTime($numRetries, $rheaders)
  538. {
  539. // Apply exponential backoff with $initialNetworkRetryDelay on the
  540. // number of $numRetries so far as inputs. Do not allow the number to exceed
  541. // $maxNetworkRetryDelay.
  542. $sleepSeconds = \min(
  543. Stripe::getInitialNetworkRetryDelay() * 1.0 * 2 ** ($numRetries - 1),
  544. Stripe::getMaxNetworkRetryDelay()
  545. );
  546. // Apply some jitter by randomizing the value in the range of
  547. // ($sleepSeconds / 2) to ($sleepSeconds).
  548. $sleepSeconds *= 0.5 * (1 + $this->randomGenerator->randFloat());
  549. // But never sleep less than the base sleep seconds.
  550. $sleepSeconds = \max(Stripe::getInitialNetworkRetryDelay(), $sleepSeconds);
  551. // And never sleep less than the time the API asks us to wait, assuming it's a reasonable ask.
  552. $retryAfter = isset($rheaders['retry-after']) ? (float) ($rheaders['retry-after']) : 0.0;
  553. if (\floor($retryAfter) === $retryAfter && $retryAfter <= Stripe::getMaxRetryAfter()) {
  554. $sleepSeconds = \max($sleepSeconds, $retryAfter);
  555. }
  556. return $sleepSeconds;
  557. }
  558. /**
  559. * Initializes the curl handle. If already initialized, the handle is closed first.
  560. */
  561. private function initCurlHandle()
  562. {
  563. $this->closeCurlHandle();
  564. $this->curlHandle = \curl_init();
  565. }
  566. /**
  567. * Closes the curl handle if initialized. Do nothing if already closed.
  568. */
  569. private function closeCurlHandle()
  570. {
  571. if (null !== $this->curlHandle) {
  572. \curl_close($this->curlHandle);
  573. $this->curlHandle = null;
  574. }
  575. }
  576. /**
  577. * Resets the curl handle. If the handle is not already initialized, or if persistent
  578. * connections are disabled, the handle is reinitialized instead.
  579. */
  580. private function resetCurlHandle()
  581. {
  582. if (null !== $this->curlHandle && $this->getEnablePersistentConnections()) {
  583. \curl_reset($this->curlHandle);
  584. } else {
  585. $this->initCurlHandle();
  586. }
  587. }
  588. /**
  589. * Indicates whether it is safe to use HTTP/2 or not.
  590. *
  591. * @return bool
  592. */
  593. private function canSafelyUseHttp2()
  594. {
  595. // Versions of curl older than 7.60.0 don't respect GOAWAY frames
  596. // (cf. https://github.com/curl/curl/issues/2416), which Stripe use.
  597. $curlVersion = \curl_version()['version'];
  598. return \version_compare($curlVersion, '7.60.0') >= 0;
  599. }
  600. /**
  601. * Checks if a list of headers contains a specific header name.
  602. *
  603. * @param string[] $headers
  604. * @param string $name
  605. *
  606. * @return bool
  607. */
  608. private function hasHeader($headers, $name)
  609. {
  610. foreach ($headers as $header) {
  611. if (0 === \strncasecmp($header, "{$name}: ", \strlen($name) + 2)) {
  612. return true;
  613. }
  614. }
  615. return false;
  616. }
  617. }