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