ic function executeStreamingRequestWithRetries($opts, $absUrl, $readBodyChunk) { /** @var bool */ $shouldRetry = false; /** @var int */ $numRetries = 0; // Will contain the bytes of the body of the last request // if it was not successful and should not be retries /** @var null|string */ $rbody = null; // Status code of the last request /** @var null|bool */ $rcode = null; // Array of headers from the last request /** @var null|array */ $lastRHeaders = null; $errno = null; $message = null; $determineWriteCallback = function ($rheaders) use ( &$readBodyChunk, &$shouldRetry, &$rbody, &$numRetries, &$rcode, &$lastRHeaders, &$errno ) { $lastRHeaders = $rheaders; $errno = \curl_errno($this->curlHandle); $rcode = \curl_getinfo($this->curlHandle, \CURLINFO_HTTP_CODE); // Send the bytes from the body of a successful request to the caller-provided $readBodyChunk. if ($rcode < 300) { $rbody = null; return function ($curl, $data) use (&$readBodyChunk) { // Don't expose the $curl handle to the user, and don't require them to // return the length of $data. \call_user_func_array($readBodyChunk, [$data]); return \strlen($data); }; } $shouldRetry = $this->shouldRetry($errno, $rcode, $rheaders, $numRetries); // Discard the body from an unsuccessful request that should be retried. if ($shouldRetry) { return function ($curl, $data) { return \strlen($data); }; } else { // Otherwise, buffer the body into $rbody. It will need to be parsed to determine // which exception to throw to the user. $rbody = ''; return function ($curl, $data) use (&$rbody) { $rbody .= $data; return \strlen($data); }; } }; while (true) { list($headerCallback, $writeCallback) = $this->useHeadersToDetermineWriteCallback($opts, $determineWriteCallback); $opts[\CURLOPT_HEADERFUNCTION] = $headerCallback; $opts[\CURLOPT_WRITEFUNCTION] = $writeCallback; $shouldRetry = false; $rbody = null; $this->resetCurlHandle(); \curl_setopt_array($this->curlHandle, $opts); $result = \curl_exec($this->curlHandle); $errno = \curl_errno($this->curlHandle); if (0 !== $errno) { $message = \curl_error($this->curlHandle); } if (!$this->getEnablePersistentConnections()) { $this->closeCurlHandle(); } if (\is_callable($this->getRequestStatusCallback())) { \call_user_func_array( $this->getRequestStatusCallback(), [$rbody, $rcode, $lastRHeaders, $errno, $message, $shouldRetry, $numRetries] ); } if ($shouldRetry) { ++$numRetries; $sleepSeconds = $this->sleepTime($numRetries, $lastRHeaders); \usleep((int) ($sleepSeconds * 1000000)); } else { break; } } if (0 !== $errno) { $this->handleCurlError($absUrl, $errno, $message, $numRetries); } return [$rbody, $rcode, $lastRHeaders]; } /** * @param array $opts cURL options * @param string $absUrl */ public function executeRequestWithRetries($opts, $absUrl) { $numRetries = 0; while (true) { $rcode = 0; $errno = 0; $message = null; // Create a callback to capture HTTP headers for the response $rheaders = new Util\CaseInsensitiveArray(); $headerCallback = function ($curl, $header_line) use (&$rheaders) { return CurlClient::parseLineIntoHeaderArray($header_line, $rheaders); }; $opts[\CURLOPT_HEADERFUNCTION] = $headerCallback; $this->resetCurlHandle(); \curl_setopt_array($this->curlHandle, $opts); $rbody = \curl_exec($this->curlHandle); if (false === $rbody) { $errno = \curl_errno($this->curlHandle); $message = \curl_error($this->curlHandle); } else { $rcode = \curl_getinfo($this->curlHandle, \CURLINFO_HTTP_CODE); } if (!$this->getEnablePersistentConnections()) { $this->closeCurlHandle(); } $shouldRetry = $this->shouldRetry($errno, $rcode, $rheaders, $numRetries); if (\is_callable($this->getRequestStatusCallback())) { \call_user_func_array( $this->getRequestStatusCallback(), [$rbody, $rcode, $rheaders, $errno, $message, $shouldRetry, $numRetries] ); } if ($shouldRetry) { ++$numRetries; $sleepSeconds = $this->sleepTime($numRetries, $rheaders); \usleep((int) ($sleepSeconds * 1000000)); } else { break; } } if (false === $rbody) { $this->handleCurlError($absUrl, $errno, $message, $numRetries); } return [$rbody, $rcode, $rheaders]; } /** * @param string $url * @param int $errno * @param string $message * @param int $numRetries * * @throws Exception\ApiConnectionException */ private function handleCurlError($url, $errno, $message, $numRetries) { switch ($errno) { case \CURLE_COULDNT_CONNECT: case \CURLE_COULDNT_RESOLVE_HOST: case \CURLE_OPERATION_TIMEOUTED: $msg = "Could not connect to Stripe ({$url}). Please check your " . 'internet connection and try again. If this problem persists, ' . "you should check Stripe's service status at " . 'https://twitter.com/stripestatus, or'; break; case \CURLE_SSL_CACERT: case \CURLE_SSL_PEER_CERTIFICATE: $msg = "Could not verify Stripe's SSL certificate. Please make sure " . 'that your network is not intercepting certificates. ' . "(Try going to {$url} in your browser.) " . 'If this problem persists,'; break; default: $msg = 'Unexpected error communicating with Stripe. ' . 'If this problem persists,'; } $msg .= ' let us know at support@stripe.com.'; $msg .= "\n\n(Network error [errno {$errno}]: {$message})"; if ($numRetries > 0) { $msg .= "\n\nRequest was retried {$numRetries} times."; } throw new Exception\ApiConnectionException($msg); } /** * Checks if an error is a problem that we should retry on. This includes both * socket errors that may represent an intermittent problem and some special * HTTP statuses. * * @param int $errno * @param int $rcode * @param array|\Stripe\Util\CaseInsensitiveArray $rheaders * @param int $numRetries * * @return bool */ private function shouldRetry($errno, $rcode, $rheaders, $numRetries) { if ($numRetries >= Stripe::getMaxNetworkRetries()) { return false; } // Retry on timeout-related problems (either on open or read). if (\CURLE_OPERATION_TIMEOUTED === $errno) { return true; } // Destination refused the connection, the connection was reset, or a // variety of other connection failures. This could occur from a single // saturated server, so retry in case it's intermittent. if (\CURLE_COULDNT_CONNECT === $errno) { return true; } // The API may ask us not to retry (eg; if doing so would be a no-op) // or advise us to retry (eg; in cases of lock timeouts); we defer to that. if (isset($rheaders['stripe-should-retry'])) { if ('false' === $rheaders['stripe-should-retry']) { return false; } if ('true' === $rheaders['stripe-should-retry']) { return true; } } // 409 Conflict if (409 === $rcode) { return true; } // Retry on 500, 503, and other internal errors. // // Note that we expect the stripe-should-retry header to be false // in most cases when a 500 is returned, since our idempotency framework // would typically replay it anyway. if ($rcode >= 500) { return true; } return false; } /** * Provides the number of seconds to wait before retrying a request. * * @param int $numRetries * @param array|\Stripe\Util\CaseInsensitiveArray $rheaders * * @return int */ private function sleepTime($numRetries, $rheaders) { // Apply exponential backoff with $initialNetworkRetryDelay on the // number of $numRetries so far as inputs. Do not allow the number to exceed // $maxNetworkRetryDelay. $sleepSeconds = \min( Stripe::getInitialNetworkRetryDelay() * 1.0 * 2 ** ($numRetries - 1), Stripe::getMaxNetworkRetryDelay() ); // Apply some jitter by randomizing the value in the range of // ($sleepSeconds / 2) to ($sleepSeconds). $sleepSeconds *= 0.5 * (1 + $this->randomGenerator->randFloat()); // But never sleep less than the base sleep seconds. $sleepSeconds = \max(Stripe::getInitialNetworkRetryDelay(), $sleepSeconds); // And never sleep less than the time the API asks us to wait, assuming it's a reasonable ask. $retryAfter = isset($rheaders['retry-after']) ? (float) ($rheaders['retry-after']) : 0.0; if (\floor($retryAfter) === $retryAfter && $retryAfter <= Stripe::getMaxRetryAfter()) { $sleepSeconds = \max($sleepSeconds, $retryAfter); } return $sleepSeconds; } /** * Initializes the curl handle. If already initialized, the handle is closed first. */ private function initCurlHandle() { $this->closeCurlHandle(); $this->curlHandle = \curl_init(); } /** * Closes the curl handle if initialized. Do nothing if already closed. */ private function closeCurlHandle() { if (null !== $this->curlHandle) { \curl_close($this->curlHandle); $this->curlHandle = null; } } /** * Resets the curl handle. If the handle is not already initialized, or if persistent * connections are disabled, the handle is reinitialized instead. */ private function resetCurlHandle() { if (null !== $this->curlHandle && $this->getEnablePersistentConnections()) { \curl_reset($this->curlHandle); } else { $this->initCurlHandle(); } } /** * Indicates whether it is safe to use HTTP/2 or not. * * @return bool */ private function canSafelyUseHttp2() { // Versions of curl older than 7.60.0 don't respect GOAWAY frames // (cf. https://github.com/curl/curl/issues/2416), which Stripe use. $curlVersion = \curl_version()['version']; return \version_compare($curlVersion, '7.60.0') >= 0; } /** * Checks if a list of headers contains a specific header name. * * @param string[] $headers * @param string $name * * @return bool */ private function hasHeader($headers, $name) { foreach ($headers as $header) { if (0 === \strncasecmp($header, "{$name}: ", \strlen($name) + 2)) { return true; } } return false; } }