3 namespace Stripe\HttpClient
;
9 // cURL constants are not defined in PHP < 5.5
11 // @codingStandardsIgnoreStart
12 // PSR2 requires all constants be upper case. Sadly, the CURL_SSLVERSION
13 // constants do not abide by those rules.
15 // Note the values 1 and 6 come from their position in the enum that
16 // defines them in cURL's source code.
17 if (!defined('CURL_SSLVERSION_TLSv1')) {
18 define('CURL_SSLVERSION_TLSv1', 1);
20 if (!defined('CURL_SSLVERSION_TLSv1_2')) {
21 define('CURL_SSLVERSION_TLSv1_2', 6);
23 // @codingStandardsIgnoreEnd
25 class CurlClient
implements ClientInterface
27 private static $instance;
29 public static function instance()
31 if (!self
::$instance) {
32 self
::$instance = new self();
34 return self
::$instance;
37 protected $defaultOptions;
39 protected $userAgentInfo;
42 * CurlClient constructor.
44 * Pass in a callable to $defaultOptions that returns an array of CURLOPT_* values to start
45 * off a request with, or an flat array with the same format used by curl_setopt_array() to
46 * provide a static set of options. Note that many options are overridden later in the request
47 * call, including timeouts, which can be set via setTimeout() and setConnectTimeout().
49 * Note that request() will silently ignore a non-callable, non-array $defaultOptions, and will
50 * throw an exception if $defaultOptions returns a non-array value.
52 * @param array|callable|null $defaultOptions
54 public function __construct($defaultOptions = null, $randomGenerator = null)
56 $this->defaultOptions
= $defaultOptions;
57 $this->randomGenerator
= $randomGenerator ?
: new Util\
RandomGenerator();
58 $this->initUserAgentInfo();
61 public function initUserAgentInfo()
63 $curlVersion = curl_version();
64 $this->userAgentInfo
= [
65 'httplib' => 'curl ' . $curlVersion['version'],
66 'ssllib' => $curlVersion['ssl_version'],
70 public function getDefaultOptions()
72 return $this->defaultOptions
;
75 public function getUserAgentInfo()
77 return $this->userAgentInfo
;
80 // USER DEFINED TIMEOUTS
82 const DEFAULT_TIMEOUT
= 80;
83 const DEFAULT_CONNECT_TIMEOUT
= 30;
85 private $timeout = self
::DEFAULT_TIMEOUT
;
86 private $connectTimeout = self
::DEFAULT_CONNECT_TIMEOUT
;
88 public function setTimeout($seconds)
90 $this->timeout
= (int) max($seconds, 0);
94 public function setConnectTimeout($seconds)
96 $this->connectTimeout
= (int) max($seconds, 0);
100 public function getTimeout()
102 return $this->timeout
;
105 public function getConnectTimeout()
107 return $this->connectTimeout
;
110 // END OF USER DEFINED TIMEOUTS
112 public function request($method, $absUrl, $headers, $params, $hasFile)
114 $method = strtolower($method);
117 if (is_callable($this->defaultOptions
)) { // call defaultOptions callback, set options to return value
118 $opts = call_user_func_array($this->defaultOptions
, func_get_args());
119 if (!is_array($opts)) {
120 throw new Error\
Api("Non-array value returned by defaultOptions CurlClient callback");
122 } elseif (is_array($this->defaultOptions
)) { // set default curlopts from array
123 $opts = $this->defaultOptions
;
126 if ($method == 'get') {
129 "Issuing a GET request with a file parameter"
132 $opts[CURLOPT_HTTPGET
] = 1;
133 if (count($params) > 0) {
134 $encoded = Util\Util
::urlEncode($params);
135 $absUrl = "$absUrl?$encoded";
137 } elseif ($method == 'post') {
138 $opts[CURLOPT_POST
] = 1;
139 $opts[CURLOPT_POSTFIELDS
] = $hasFile ?
$params : Util\Util
::urlEncode($params);
140 } elseif ($method == 'delete') {
141 $opts[CURLOPT_CUSTOMREQUEST
] = 'DELETE';
142 if (count($params) > 0) {
143 $encoded = Util\Util
::urlEncode($params);
144 $absUrl = "$absUrl?$encoded";
147 throw new Error\
Api("Unrecognized method $method");
150 // It is only safe to retry network failures on POST requests if we
151 // add an Idempotency-Key header
152 if (($method == 'post') && (Stripe
::$maxNetworkRetries > 0)) {
153 if (!isset($headers['Idempotency-Key'])) {
154 array_push($headers, 'Idempotency-Key: ' . $this->randomGenerator
->uuid());
158 // Create a callback to capture HTTP headers for the response
160 $headerCallback = function ($curl, $header_line) use (&$rheaders) {
161 // Ignore the HTTP request line (HTTP/1.1 200 OK)
162 if (strpos($header_line, ":") === false) {
163 return strlen($header_line);
165 list($key, $value) = explode(":", trim($header_line), 2);
166 $rheaders[trim($key)] = trim($value);
167 return strlen($header_line);
170 // By default for large request body sizes (> 1024 bytes), cURL will
171 // send a request without a body and with a `Expect: 100-continue`
172 // header, which gives the server a chance to respond with an error
173 // status code in cases where one can be determined right away (say
174 // on an authentication problem for example), and saves the "large"
175 // request body from being ever sent.
177 // Unfortunately, the bindings don't currently correctly handle the
178 // success case (in which the server sends back a 100 CONTINUE), so
179 // we'll error under that condition. To compensate for that problem
180 // for the time being, override cURL's behavior by simply always
181 // sending an empty `Expect:` header.
182 array_push($headers, 'Expect: ');
184 $absUrl = Util\Util
::utf8($absUrl);
185 $opts[CURLOPT_URL
] = $absUrl;
186 $opts[CURLOPT_RETURNTRANSFER
] = true;
187 $opts[CURLOPT_CONNECTTIMEOUT
] = $this->connectTimeout
;
188 $opts[CURLOPT_TIMEOUT
] = $this->timeout
;
189 $opts[CURLOPT_HEADERFUNCTION
] = $headerCallback;
190 $opts[CURLOPT_HTTPHEADER
] = $headers;
191 $opts[CURLOPT_CAINFO
] = Stripe
::getCABundlePath();
192 if (!Stripe
::getVerifySslCerts()) {
193 $opts[CURLOPT_SSL_VERIFYPEER
] = false;
196 list($rbody, $rcode) = $this->executeRequestWithRetries($opts, $absUrl);
198 return [$rbody, $rcode, $rheaders];
202 * @param array $opts cURL options
204 private function executeRequestWithRetries($opts, $absUrl)
213 curl_setopt_array($curl, $opts);
214 $rbody = curl_exec($curl);
216 if ($rbody === false) {
217 $errno = curl_errno($curl);
218 $message = curl_error($curl);
220 $rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE
);
224 if ($this->shouldRetry($errno, $rcode, $numRetries)) {
226 $sleepSeconds = $this->sleepTime($numRetries);
227 usleep(intval($sleepSeconds * 1000000));
233 if ($rbody === false) {
234 $this->handleCurlError($absUrl, $errno, $message, $numRetries);
237 return [$rbody, $rcode];
243 * @param string $message
244 * @param int $numRetries
245 * @throws Error\ApiConnection
247 private function handleCurlError($url, $errno, $message, $numRetries)
250 case CURLE_COULDNT_CONNECT
:
251 case CURLE_COULDNT_RESOLVE_HOST
:
252 case CURLE_OPERATION_TIMEOUTED
:
253 $msg = "Could not connect to Stripe ($url). Please check your "
254 . "internet connection and try again. If this problem persists, "
255 . "you should check Stripe's service status at "
256 . "https://twitter.com/stripestatus, or";
258 case CURLE_SSL_CACERT
:
259 case CURLE_SSL_PEER_CERTIFICATE
:
260 $msg = "Could not verify Stripe's SSL certificate. Please make sure "
261 . "that your network is not intercepting certificates. "
262 . "(Try going to $url in your browser.) "
263 . "If this problem persists,";
266 $msg = "Unexpected error communicating with Stripe. "
267 . "If this problem persists,";
269 $msg .= " let us know at support@stripe.com.";
271 $msg .= "\n\n(Network error [errno $errno]: $message)";
273 if ($numRetries > 0) {
274 $msg .= "\n\nRequest was retried $numRetries times.";
277 throw new Error\
ApiConnection($msg);
281 * Checks if an error is a problem that we should retry on. This includes both
282 * socket errors that may represent an intermittent problem and some special
286 * @param int $numRetries
289 private function shouldRetry($errno, $rcode, $numRetries)
291 if ($numRetries >= Stripe
::getMaxNetworkRetries()) {
295 // Retry on timeout-related problems (either on open or read).
296 if ($errno === CURLE_OPERATION_TIMEOUTED
) {
300 // Destination refused the connection, the connection was reset, or a
301 // variety of other connection failures. This could occur from a single
302 // saturated server, so retry in case it's intermittent.
303 if ($errno === CURLE_COULDNT_CONNECT
) {
308 if ($rcode === 409) {
315 private function sleepTime($numRetries)
317 // Apply exponential backoff with $initialNetworkRetryDelay on the
318 // number of $numRetries so far as inputs. Do not allow the number to exceed
319 // $maxNetworkRetryDelay.
321 Stripe
::getInitialNetworkRetryDelay() * 1.0 * pow(2, $numRetries - 1),
322 Stripe
::getMaxNetworkRetryDelay()
325 // Apply some jitter by randomizing the value in the range of
326 // ($sleepSeconds / 2) to ($sleepSeconds).
327 $sleepSeconds *= 0.5 * (1 +
$this->randomGenerator
->randFloat());
329 // But never sleep less than the base sleep seconds.
330 $sleepSeconds = max(Stripe
::getInitialNetworkRetryDelay(), $sleepSeconds);
332 return $sleepSeconds;