• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stripe / stripe-php / 11129599820

01 Oct 2024 04:33PM UTC coverage: 62.613% (-1.3%) from 63.944%
11129599820

push

github

web-flow
Support for APIs in the new API version 2024-09-30.acacia (#1756)

175 of 409 new or added lines in 26 files covered. (42.79%)

3 existing lines in 3 files now uncovered.

3547 of 5665 relevant lines covered (62.61%)

2.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.67
/lib/HttpClient/CurlClient.php
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()
31✔
32
    {
33
        if (!static::$instance) {
31✔
34
            static::$instance = new static();
×
35
        }
36

37
        return static::$instance;
31✔
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)
30✔
70
    {
71
        $this->defaultOptions = $defaultOptions;
30✔
72
        $this->randomGenerator = $randomGenerator ?: new Util\RandomGenerator();
30✔
73
        $this->initUserAgentInfo();
30✔
74

75
        $this->enableHttp2 = $this->canSafelyUseHttp2();
30✔
76
    }
77

78
    public function __destruct()
27✔
79
    {
80
        $this->closeCurlHandle();
27✔
81
    }
82

83
    public function initUserAgentInfo()
30✔
84
    {
85
        $curlVersion = \curl_version();
30✔
86
        $this->userAgentInfo = [
30✔
87
            'httplib' => 'curl ' . $curlVersion['version'],
30✔
88
            'ssllib' => $curlVersion['ssl_version'],
30✔
89
        ];
30✔
90
    }
91

92
    public function getDefaultOptions()
3✔
93
    {
94
        return $this->defaultOptions;
3✔
95
    }
96

97
    public function getUserAgentInfo()
4✔
98
    {
99
        return $this->userAgentInfo;
4✔
100
    }
101

102
    /**
103
     * @return bool
104
     */
105
    public function getEnablePersistentConnections()
6✔
106
    {
107
        return $this->enablePersistentConnections;
6✔
108
    }
109

110
    /**
111
     * @param bool $enable
112
     */
113
    public function setEnablePersistentConnections($enable)
1✔
114
    {
115
        $this->enablePersistentConnections = $enable;
1✔
116
    }
117

118
    /**
119
     * @return bool
120
     */
121
    public function getEnableHttp2()
11✔
122
    {
123
        return $this->enableHttp2;
11✔
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()
6✔
138
    {
139
        return $this->requestStatusCallback;
6✔
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)
2✔
158
    {
159
        $this->requestStatusCallback = $requestStatusCallback;
2✔
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)
1✔
171
    {
172
        $this->timeout = (int) \max($seconds, 0);
1✔
173

174
        return $this;
1✔
175
    }
176

177
    public function setConnectTimeout($seconds)
1✔
178
    {
179
        $this->connectTimeout = (int) \max($seconds, 0);
1✔
180

181
        return $this;
1✔
182
    }
183

184
    public function getTimeout()
1✔
185
    {
186
        return $this->timeout;
1✔
187
    }
188

189
    public function getConnectTimeout()
1✔
190
    {
191
        return $this->connectTimeout;
1✔
192
    }
193

194
    // END OF USER DEFINED TIMEOUTS
195

196
    /**
197
     * @param 'delete'|'get'|'post' $method
198
     * @param string $absUrl
199
     * @param string $params
200
     * @param bool $hasFile
201
     * @param 'v1'|'v2' $apiMode
202
     */
203
    private function constructUrlAndBody($method, $absUrl, $params, $hasFile, $apiMode)
4✔
204
    {
205
        $params = Util\Util::objectsToIds($params);
4✔
206
        if ('post' === $method) {
4✔
NEW
207
            $absUrl = Util\Util::utf8($absUrl);
×
NEW
208
            if ($hasFile) {
×
NEW
209
                return [$absUrl, $params];
×
210
            }
NEW
211
            if ('v2' === $apiMode) {
×
NEW
212
                if (\is_array($params) && 0 === \count($params)) {
×
213
                    // Send a request with empty body if we have no params set
214
                    // Setting the second parameter as null prevents the CURLOPT_POSTFIELDS
215
                    // from being set with the '[]', which is result of `json_encode([]).
NEW
216
                    return [$absUrl, null];
×
217
                }
218

NEW
219
                return [$absUrl, \json_encode($params)];
×
220
            }
221

NEW
222
            return [$absUrl, Util\Util::encodeParameters($params)];
×
223
        }
224
        if ($hasFile) {
4✔
NEW
225
            throw new Exception\UnexpectedValueException("Unexpected. {$method} methods don't support file attachments");
×
226
        }
227
        if (0 === \count($params)) {
4✔
228
            return [Util\Util::utf8($absUrl), null];
4✔
229
        }
NEW
230
        $encoded = Util\Util::encodeParameters($params, $apiMode);
×
231

NEW
232
        $absUrl = "{$absUrl}?{$encoded}";
×
NEW
233
        $absUrl = Util\Util::utf8($absUrl);
×
234

NEW
235
        return [$absUrl, null];
×
236
    }
237

238
    private function calculateDefaultOptions($method, $absUrl, $headers, $params, $hasFile)
4✔
239
    {
240
        if (\is_callable($this->defaultOptions)) { // call defaultOptions callback, set options to return value
4✔
241
            $ret = \call_user_func_array($this->defaultOptions, [$method, $absUrl, $headers, $params, $hasFile]);
1✔
242
            if (!\is_array($ret)) {
1✔
243
                throw new Exception\UnexpectedValueException('Non-array value returned by defaultOptions CurlClient callback');
1✔
244
            }
245

246
            return $ret;
1✔
247
        }
248
        if (\is_array($this->defaultOptions)) { // set default curlopts from array
3✔
NEW
249
            return $this->defaultOptions;
×
250
        }
251

252
        return [];
3✔
253
    }
254

255
    private function constructCurlOptions($method, $absUrl, $headers, $body, $opts, $apiMode)
11✔
256
    {
257
        if ('get' === $method) {
11✔
258
            $opts[\CURLOPT_HTTPGET] = 1;
4✔
259
        } elseif ('post' === $method) {
7✔
260
            $opts[\CURLOPT_POST] = 1;
4✔
261
        } elseif ('delete' === $method) {
3✔
262
            $opts[\CURLOPT_CUSTOMREQUEST] = 'DELETE';
3✔
263
        } else {
264
            throw new Exception\UnexpectedValueException("Unrecognized method {$method}");
×
265
        }
266

267
        if ($body) {
11✔
NEW
268
            $opts[\CURLOPT_POSTFIELDS] = $body;
×
269
        }
270
        // this is a little verbose, but makes v1 vs v2 behavior really clear
271
        if (!$this->hasHeader($headers, 'Idempotency-Key')) {
11✔
272
            // all v2 requests should have an IK
273
            if ('v2' === $apiMode) {
11✔
274
                if ('post' === $method || 'delete' === $method) {
3✔
275
                    $headers[] = 'Idempotency-Key: ' . $this->randomGenerator->uuid();
3✔
276
                }
277
            } else {
278
                // v1 requests should keep old behavior for consistency
279
                if ('post' === $method && Stripe::$maxNetworkRetries > 0) {
8✔
280
                    $headers[] = 'Idempotency-Key: ' . $this->randomGenerator->uuid();
1✔
281
                }
282
            }
283
        }
284

285
        // By default for large request body sizes (> 1024 bytes), cURL will
286
        // send a request without a body and with a `Expect: 100-continue`
287
        // header, which gives the server a chance to respond with an error
288
        // status code in cases where one can be determined right away (say
289
        // on an authentication problem for example), and saves the "large"
290
        // request body from being ever sent.
291
        //
292
        // Unfortunately, the bindings don't currently correctly handle the
293
        // success case (in which the server sends back a 100 CONTINUE), so
294
        // we'll error under that condition. To compensate for that problem
295
        // for the time being, override cURL's behavior by simply always
296
        // sending an empty `Expect:` header.
297
        $headers[] = 'Expect: ';
11✔
298

299
        $opts[\CURLOPT_URL] = $absUrl;
11✔
300
        $opts[\CURLOPT_RETURNTRANSFER] = true;
11✔
301
        $opts[\CURLOPT_CONNECTTIMEOUT] = $this->connectTimeout;
11✔
302
        $opts[\CURLOPT_TIMEOUT] = $this->timeout;
11✔
303
        $opts[\CURLOPT_HTTPHEADER] = $headers;
11✔
304
        $opts[\CURLOPT_CAINFO] = Stripe::getCABundlePath();
11✔
305
        if (!Stripe::getVerifySslCerts()) {
11✔
306
            $opts[\CURLOPT_SSL_VERIFYPEER] = false;
×
307
        }
308

309
        if (!isset($opts[\CURLOPT_HTTP_VERSION]) && $this->getEnableHttp2()) {
11✔
310
            // For HTTPS requests, enable HTTP/2, if supported
311
            $opts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2TLS;
11✔
312
        }
313

314
        return $opts;
11✔
315
    }
316

317
    /**
318
     * @param 'delete'|'get'|'post' $method
319
     * @param string $absUrl
320
     * @param array $headers
321
     * @param array $params
322
     * @param bool $hasFile
323
     * @param 'v1'|'v2' $apiMode
324
     */
325
    private function constructRequest($method, $absUrl, $headers, $params, $hasFile, $apiMode)
4✔
326
    {
327
        $method = \strtolower($method);
4✔
328

329
        $opts = $this->calculateDefaultOptions($method, $absUrl, $headers, $params, $hasFile);
4✔
330
        list($absUrl, $body) = $this->constructUrlAndBody($method, $absUrl, $params, $hasFile, $apiMode);
4✔
331
        $opts = $this->constructCurlOptions($method, $absUrl, $headers, $body, $opts, $apiMode);
4✔
332

333
        return [$opts, $absUrl];
4✔
334
    }
335

336
    /**
337
     * @param 'delete'|'get'|'post' $method
338
     * @param string $absUrl
339
     * @param array $headers
340
     * @param array $params
341
     * @param bool $hasFile
342
     * @param 'v1'|'v2' $apiMode
343
     */
344
    public function request($method, $absUrl, $headers, $params, $hasFile, $apiMode = 'v1')
4✔
345
    {
346
        list($opts, $absUrl) = $this->constructRequest($method, $absUrl, $headers, $params, $hasFile, $apiMode);
4✔
347
        list($rbody, $rcode, $rheaders) = $this->executeRequestWithRetries($opts, $absUrl);
4✔
348

349
        return [$rbody, $rcode, $rheaders];
4✔
350
    }
351

352
    /**
353
     * @param 'delete'|'get'|'post' $method
354
     * @param string $absUrl
355
     * @param array $headers
356
     * @param array $params
357
     * @param bool $hasFile
358
     * @param callable $readBodyChunk
359
     * @param 'v1'|'v2' $apiMode
360
     */
NEW
361
    public function requestStream($method, $absUrl, $headers, $params, $hasFile, $readBodyChunk, $apiMode = 'v1')
×
362
    {
NEW
363
        list($opts, $absUrl) = $this->constructRequest($method, $absUrl, $headers, $params, $hasFile, $apiMode);
×
UNCOV
364
        $opts[\CURLOPT_RETURNTRANSFER] = false;
×
365
        list($rbody, $rcode, $rheaders) = $this->executeStreamingRequestWithRetries($opts, $absUrl, $readBodyChunk);
×
366

367
        return [$rbody, $rcode, $rheaders];
×
368
    }
369

370
    /**
371
     * Curl permits sending \CURLOPT_HEADERFUNCTION, which is called with lines
372
     * from the header and \CURLOPT_WRITEFUNCTION, which is called with bytes
373
     * from the body. You usually want to handle the body differently depending
374
     * on what was in the header.
375
     *
376
     * This function makes it easier to specify different callbacks depending
377
     * on the contents of the heeder. After the header has been completely read
378
     * and the body begins to stream, it will call $determineWriteCallback with
379
     * the array of headers. $determineWriteCallback should, based on the
380
     * headers it receives, return a "writeCallback" that describes what to do
381
     * with the incoming HTTP response body.
382
     *
383
     * @param array $opts
384
     * @param callable $determineWriteCallback
385
     *
386
     * @return array
387
     */
388
    private function useHeadersToDetermineWriteCallback($opts, $determineWriteCallback)
3✔
389
    {
390
        $rheaders = new Util\CaseInsensitiveArray();
3✔
391
        $headerCallback = function ($curl, $header_line) use (&$rheaders) {
3✔
392
            return self::parseLineIntoHeaderArray($header_line, $rheaders);
3✔
393
        };
3✔
394

395
        $writeCallback = null;
3✔
396
        $writeCallbackWrapper = function ($curl, $data) use (&$writeCallback, &$rheaders, &$determineWriteCallback) {
3✔
397
            if (null === $writeCallback) {
3✔
398
                $writeCallback = \call_user_func_array($determineWriteCallback, [$rheaders]);
3✔
399
            }
400

401
            return \call_user_func_array($writeCallback, [$curl, $data]);
3✔
402
        };
3✔
403

404
        return [$headerCallback, $writeCallbackWrapper];
3✔
405
    }
406

407
    private static function parseLineIntoHeaderArray($line, &$headers)
6✔
408
    {
409
        if (false === \strpos($line, ':')) {
6✔
410
            return \strlen($line);
6✔
411
        }
412
        list($key, $value) = \explode(':', \trim($line), 2);
6✔
413
        $headers[\trim($key)] = \trim($value);
6✔
414

415
        return \strlen($line);
6✔
416
    }
417

418
    /**
419
     * Like `executeRequestWithRetries` except:
420
     *   1. Does not buffer the body of a successful (status code < 300)
421
     *      response into memory -- instead, calls the caller-provided
422
     *      $readBodyChunk with each chunk of incoming data.
423
     *   2. Does not retry if a network error occurs while streaming the
424
     *      body of a successful response.
425
     *
426
     * @param array $opts cURL options
427
     * @param string $absUrl
428
     * @param callable $readBodyChunk
429
     *
430
     * @return array
431
     */
432
    public function executeStreamingRequestWithRetries($opts, $absUrl, $readBodyChunk)
3✔
433
    {
434
        /** @var bool */
435
        $shouldRetry = false;
3✔
436
        /** @var int */
437
        $numRetries = 0;
3✔
438

439
        // Will contain the bytes of the body of the last request
440
        // if it was not successful and should not be retries
441
        /** @var null|string */
442
        $rbody = null;
3✔
443

444
        // Status code of the last request
445
        /** @var null|bool */
446
        $rcode = null;
3✔
447

448
        // Array of headers from the last request
449
        /** @var null|array */
450
        $lastRHeaders = null;
3✔
451

452
        $errno = null;
3✔
453
        $message = null;
3✔
454

455
        $determineWriteCallback = function ($rheaders) use (&$readBodyChunk, &$shouldRetry, &$rbody, &$numRetries, &$rcode, &$lastRHeaders, &$errno) {
3✔
456
            $lastRHeaders = $rheaders;
3✔
457
            $errno = \curl_errno($this->curlHandle);
3✔
458

459
            $rcode = \curl_getinfo($this->curlHandle, \CURLINFO_HTTP_CODE);
3✔
460

461
            // Send the bytes from the body of a successful request to the caller-provided $readBodyChunk.
462
            if ($rcode < 300) {
3✔
463
                $rbody = null;
2✔
464

465
                return function ($curl, $data) use (&$readBodyChunk) {
2✔
466
                    // Don't expose the $curl handle to the user, and don't require them to
467
                    // return the length of $data.
468
                    \call_user_func_array($readBodyChunk, [$data]);
2✔
469

470
                    return \strlen($data);
2✔
471
                };
2✔
472
            }
473

474
            $shouldRetry = $this->shouldRetry($errno, $rcode, $rheaders, $numRetries);
1✔
475

476
            // Discard the body from an unsuccessful request that should be retried.
477
            if ($shouldRetry) {
1✔
478
                return function ($curl, $data) {
1✔
479
                    return \strlen($data);
1✔
480
                };
1✔
481
            } else {
482
                // Otherwise, buffer the body into $rbody. It will need to be parsed to determine
483
                // which exception to throw to the user.
484
                $rbody = '';
1✔
485

486
                return function ($curl, $data) use (&$rbody) {
1✔
487
                    $rbody .= $data;
1✔
488

489
                    return \strlen($data);
1✔
490
                };
1✔
491
            }
492
        };
3✔
493

494
        while (true) {
3✔
495
            list($headerCallback, $writeCallback) = $this->useHeadersToDetermineWriteCallback($opts, $determineWriteCallback);
3✔
496
            $opts[\CURLOPT_HEADERFUNCTION] = $headerCallback;
3✔
497
            $opts[\CURLOPT_WRITEFUNCTION] = $writeCallback;
3✔
498

499
            $shouldRetry = false;
3✔
500
            $rbody = null;
3✔
501
            $this->resetCurlHandle();
3✔
502
            \curl_setopt_array($this->curlHandle, $opts);
3✔
503
            $result = \curl_exec($this->curlHandle);
3✔
504
            $errno = \curl_errno($this->curlHandle);
3✔
505
            if (0 !== $errno) {
3✔
506
                $message = \curl_error($this->curlHandle);
1✔
507
            }
508
            if (!$this->getEnablePersistentConnections()) {
3✔
509
                $this->closeCurlHandle();
1✔
510
            }
511

512
            if (\is_callable($this->getRequestStatusCallback())) {
3✔
513
                \call_user_func_array(
1✔
514
                    $this->getRequestStatusCallback(),
1✔
515
                    [$rbody, $rcode, $lastRHeaders, $errno, $message, $shouldRetry, $numRetries]
1✔
516
                );
1✔
517
            }
518

519
            if ($shouldRetry) {
3✔
520
                ++$numRetries;
1✔
521
                $sleepSeconds = $this->sleepTime($numRetries, $lastRHeaders);
1✔
522
                \usleep((int) ($sleepSeconds * 1000000));
1✔
523
            } else {
524
                break;
3✔
525
            }
526
        }
527

528
        if (0 !== $errno) {
3✔
529
            $this->handleCurlError($absUrl, $errno, $message, $numRetries);
1✔
530
        }
531

532
        return [$rbody, $rcode, $lastRHeaders];
2✔
533
    }
534

535
    /**
536
     * @param array $opts cURL options
537
     * @param string $absUrl
538
     */
539
    public function executeRequestWithRetries($opts, $absUrl)
4✔
540
    {
541
        $numRetries = 0;
4✔
542

543
        while (true) {
4✔
544
            $rcode = 0;
4✔
545
            $errno = 0;
4✔
546
            $message = null;
4✔
547

548
            // Create a callback to capture HTTP headers for the response
549
            $rheaders = new Util\CaseInsensitiveArray();
4✔
550
            $headerCallback = function ($curl, $header_line) use (&$rheaders) {
4✔
551
                return CurlClient::parseLineIntoHeaderArray($header_line, $rheaders);
4✔
552
            };
4✔
553
            $opts[\CURLOPT_HEADERFUNCTION] = $headerCallback;
4✔
554

555
            $this->resetCurlHandle();
4✔
556
            \curl_setopt_array($this->curlHandle, $opts);
4✔
557
            $rbody = \curl_exec($this->curlHandle);
4✔
558

559
            if (false === $rbody) {
4✔
560
                $errno = \curl_errno($this->curlHandle);
×
561
                $message = \curl_error($this->curlHandle);
×
562
            } else {
563
                $rcode = \curl_getinfo($this->curlHandle, \CURLINFO_HTTP_CODE);
4✔
564
            }
565
            if (!$this->getEnablePersistentConnections()) {
4✔
566
                $this->closeCurlHandle();
×
567
            }
568

569
            $shouldRetry = $this->shouldRetry($errno, $rcode, $rheaders, $numRetries);
4✔
570

571
            if (\is_callable($this->getRequestStatusCallback())) {
4✔
572
                \call_user_func_array(
1✔
573
                    $this->getRequestStatusCallback(),
1✔
574
                    [$rbody, $rcode, $rheaders, $errno, $message, $shouldRetry, $numRetries]
1✔
575
                );
1✔
576
            }
577

578
            if ($shouldRetry) {
4✔
579
                ++$numRetries;
×
580
                $sleepSeconds = $this->sleepTime($numRetries, $rheaders);
×
581
                \usleep((int) ($sleepSeconds * 1000000));
×
582
            } else {
583
                break;
4✔
584
            }
585
        }
586

587
        if (false === $rbody) {
4✔
588
            $this->handleCurlError($absUrl, $errno, $message, $numRetries);
×
589
        }
590

591
        return [$rbody, $rcode, $rheaders];
4✔
592
    }
593

594
    /**
595
     * @param string $url
596
     * @param int $errno
597
     * @param string $message
598
     * @param int $numRetries
599
     *
600
     * @throws Exception\ApiConnectionException
601
     */
602
    private function handleCurlError($url, $errno, $message, $numRetries)
1✔
603
    {
604
        switch ($errno) {
605
            case \CURLE_COULDNT_CONNECT:
606
            case \CURLE_COULDNT_RESOLVE_HOST:
607
            case \CURLE_OPERATION_TIMEOUTED:
608
                $msg = "Could not connect to Stripe ({$url}).  Please check your "
×
NEW
609
                    . 'internet connection and try again.  If this problem persists, '
×
NEW
610
                    . "you should check Stripe's service status at "
×
NEW
611
                    . 'https://twitter.com/stripestatus, or';
×
612

613
                break;
×
614

615
            case \CURLE_SSL_CACERT:
616
            case \CURLE_SSL_PEER_CERTIFICATE:
617
                $msg = "Could not verify Stripe's SSL certificate.  Please make sure "
×
NEW
618
                    . 'that your network is not intercepting certificates.  '
×
NEW
619
                    . "(Try going to {$url} in your browser.)  "
×
NEW
620
                    . 'If this problem persists,';
×
621

622
                break;
×
623

624
            default:
625
                $msg = 'Unexpected error communicating with Stripe.  '
1✔
626
                    . 'If this problem persists,';
1✔
627
        }
628
        $msg .= ' let us know at support@stripe.com.';
1✔
629

630
        $msg .= "\n\n(Network error [errno {$errno}]: {$message})";
1✔
631

632
        if ($numRetries > 0) {
1✔
633
            $msg .= "\n\nRequest was retried {$numRetries} times.";
×
634
        }
635

636
        throw new Exception\ApiConnectionException($msg);
1✔
637
    }
638

639
    /**
640
     * Checks if an error is a problem that we should retry on. This includes both
641
     * socket errors that may represent an intermittent problem and some special
642
     * HTTP statuses.
643
     *
644
     * @param int $errno
645
     * @param int $rcode
646
     * @param array|\Stripe\Util\CaseInsensitiveArray $rheaders
647
     * @param int $numRetries
648
     *
649
     * @return bool
650
     */
651
    private function shouldRetry($errno, $rcode, $rheaders, $numRetries)
15✔
652
    {
653
        if ($numRetries >= Stripe::getMaxNetworkRetries()) {
15✔
654
            return false;
6✔
655
        }
656

657
        // Retry on timeout-related problems (either on open or read).
658
        if (\CURLE_OPERATION_TIMEOUTED === $errno) {
10✔
659
            return true;
1✔
660
        }
661

662
        // Destination refused the connection, the connection was reset, or a
663
        // variety of other connection failures. This could occur from a single
664
        // saturated server, so retry in case it's intermittent.
665
        if (\CURLE_COULDNT_CONNECT === $errno) {
9✔
666
            return true;
1✔
667
        }
668

669
        // The API may ask us not to retry (eg; if doing so would be a no-op)
670
        // or advise us to retry (eg; in cases of lock timeouts); we defer to that.
671
        if (isset($rheaders['stripe-should-retry'])) {
8✔
672
            if ('false' === $rheaders['stripe-should-retry']) {
3✔
673
                return false;
1✔
674
            }
675
            if ('true' === $rheaders['stripe-should-retry']) {
2✔
676
                return true;
2✔
677
            }
678
        }
679

680
        // 409 Conflict
681
        if (409 === $rcode) {
7✔
682
            return true;
1✔
683
        }
684

685
        // Retry on 500, 503, and other internal errors.
686
        //
687
        // Note that we expect the stripe-should-retry header to be false
688
        // in most cases when a 500 is returned, since our idempotency framework
689
        // would typically replay it anyway.
690
        if ($rcode >= 500) {
6✔
691
            return true;
3✔
692
        }
693

694
        return false;
3✔
695
    }
696

697
    /**
698
     * Provides the number of seconds to wait before retrying a request.
699
     *
700
     * @param int $numRetries
701
     * @param array|\Stripe\Util\CaseInsensitiveArray $rheaders
702
     *
703
     * @return int
704
     */
705
    private function sleepTime($numRetries, $rheaders)
5✔
706
    {
707
        // Apply exponential backoff with $initialNetworkRetryDelay on the
708
        // number of $numRetries so far as inputs. Do not allow the number to exceed
709
        // $maxNetworkRetryDelay.
710
        $sleepSeconds = \min(
5✔
711
            Stripe::getInitialNetworkRetryDelay() * 1.0 * 2 ** ($numRetries - 1),
5✔
712
            Stripe::getMaxNetworkRetryDelay()
5✔
713
        );
5✔
714

715
        // Apply some jitter by randomizing the value in the range of
716
        // ($sleepSeconds / 2) to ($sleepSeconds).
717
        $sleepSeconds *= 0.5 * (1 + $this->randomGenerator->randFloat());
5✔
718

719
        // But never sleep less than the base sleep seconds.
720
        $sleepSeconds = \max(Stripe::getInitialNetworkRetryDelay(), $sleepSeconds);
5✔
721

722
        // And never sleep less than the time the API asks us to wait, assuming it's a reasonable ask.
723
        $retryAfter = isset($rheaders['retry-after']) ? (float) ($rheaders['retry-after']) : 0.0;
5✔
724
        if (\floor($retryAfter) === $retryAfter && $retryAfter <= Stripe::getMaxRetryAfter()) {
5✔
725
            $sleepSeconds = \max($sleepSeconds, $retryAfter);
5✔
726
        }
727

728
        return $sleepSeconds;
5✔
729
    }
730

731
    /**
732
     * Initializes the curl handle. If already initialized, the handle is closed first.
733
     */
734
    private function initCurlHandle()
5✔
735
    {
736
        $this->closeCurlHandle();
5✔
737
        $this->curlHandle = \curl_init();
5✔
738
    }
739

740
    /**
741
     * Closes the curl handle if initialized. Do nothing if already closed.
742
     */
743
    private function closeCurlHandle()
30✔
744
    {
745
        if (null !== $this->curlHandle) {
30✔
746
            \curl_close($this->curlHandle);
1✔
747
            $this->curlHandle = null;
1✔
748
        }
749
    }
750

751
    /**
752
     * Resets the curl handle. If the handle is not already initialized, or if persistent
753
     * connections are disabled, the handle is reinitialized instead.
754
     */
755
    private function resetCurlHandle()
6✔
756
    {
757
        if (null !== $this->curlHandle && $this->getEnablePersistentConnections()) {
6✔
758
            \curl_reset($this->curlHandle);
3✔
759
        } else {
760
            $this->initCurlHandle();
5✔
761
        }
762
    }
763

764
    /**
765
     * Indicates whether it is safe to use HTTP/2 or not.
766
     *
767
     * @return bool
768
     */
769
    private function canSafelyUseHttp2()
30✔
770
    {
771
        // Versions of curl older than 7.60.0 don't respect GOAWAY frames
772
        // (cf. https://github.com/curl/curl/issues/2416), which Stripe use.
773
        $curlVersion = \curl_version()['version'];
30✔
774

775
        return \version_compare($curlVersion, '7.60.0') >= 0;
30✔
776
    }
777

778
    /**
779
     * Checks if a list of headers contains a specific header name.
780
     *
781
     * @param string[] $headers
782
     * @param string $name
783
     *
784
     * @return bool
785
     */
786
    private function hasHeader($headers, $name)
11✔
787
    {
788
        foreach ($headers as $header) {
11✔
789
            if (0 === \strncasecmp($header, "{$name}: ", \strlen($name) + 2)) {
3✔
790
                return true;
×
791
            }
792
        }
793

794
        return false;
11✔
795
    }
796
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc