• 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

66.55
/lib/ApiRequestor.php
1
<?php
2

3
namespace Stripe;
4

5
/**
6
 * Class ApiRequestor.
7
 */
8
class ApiRequestor
9
{
10
    /**
11
     * @var null|string
12
     */
13
    private $_apiKey;
14

15
    /**
16
     * @var string
17
     */
18
    private $_apiBase;
19

20
    /**
21
     * @var null|array
22
     */
23
    private $_appInfo;
24

25
    /**
26
     * @var HttpClient\ClientInterface
27
     */
28
    private static $_httpClient;
29
    /**
30
     * @var HttpClient\StreamingClientInterface
31
     */
32
    private static $_streamingHttpClient;
33

34
    /**
35
     * @var RequestTelemetry
36
     */
37
    private static $requestTelemetry;
38

39
    private static $OPTIONS_KEYS = ['api_key', 'idempotency_key', 'stripe_account', 'stripe_context', 'stripe_version', 'api_base'];
40

41
    /**
42
     * ApiRequestor constructor.
43
     *
44
     * @param null|string $apiKey
45
     * @param null|string $apiBase
46
     * @param null|array $appInfo
47
     */
48
    public function __construct($apiKey = null, $apiBase = null, $appInfo = null)
24✔
49
    {
50
        $this->_apiKey = $apiKey;
24✔
51
        if (!$apiBase) {
24✔
52
            $apiBase = Stripe::$apiBase;
1✔
53
        }
54
        $this->_apiBase = $apiBase;
24✔
55
        $this->_appInfo = $appInfo;
24✔
56
    }
57

58
    /**
59
     * Creates a telemetry json blob for use in 'X-Stripe-Client-Telemetry' headers.
60
     *
61
     * @static
62
     *
63
     * @param RequestTelemetry $requestTelemetry
64
     *
65
     * @return string
66
     */
67
    private static function _telemetryJson($requestTelemetry)
×
68
    {
69
        $payload = [
×
70
            'last_request_metrics' => [
×
71
                'request_id' => $requestTelemetry->requestId,
×
72
                'request_duration_ms' => $requestTelemetry->requestDuration,
×
73
            ],
×
74
        ];
×
75
        if (\count($requestTelemetry->usage) > 0) {
×
76
            $payload['last_request_metrics']['usage'] = $requestTelemetry->usage;
×
77
        }
78

79
        $result = \json_encode($payload);
×
80
        if (false !== $result) {
×
81
            return $result;
×
82
        }
83
        Stripe::getLogger()->error('Serializing telemetry payload failed!');
×
84

85
        return '{}';
×
86
    }
87

88
    /**
89
     * @static
90
     *
91
     * @param ApiResource|array|bool|mixed $d
92
     *
93
     * @return ApiResource|array|mixed|string
94
     */
95
    private static function _encodeObjects($d)
20✔
96
    {
97
        if ($d instanceof ApiResource) {
20✔
98
            return Util\Util::utf8($d->id);
1✔
99
        }
100
        if (true === $d) {
20✔
101
            return 'true';
1✔
102
        }
103
        if (false === $d) {
20✔
104
            return 'false';
1✔
105
        }
106
        if (\is_array($d)) {
20✔
107
            $res = [];
20✔
108
            foreach ($d as $k => $v) {
20✔
109
                $res[$k] = self::_encodeObjects($v);
1✔
110
            }
111

112
            return $res;
20✔
113
        }
114

115
        return Util\Util::utf8($d);
1✔
116
    }
117

118
    /**
119
     * @param 'delete'|'get'|'post'     $method
120
     * @param string     $url
121
     * @param null|array $params
122
     * @param null|array $headers
123
     * @param 'v1'|'v2' $apiMode
124
     * @param string[] $usage
125
     *
126
     * @throws Exception\ApiErrorException
127
     *
128
     * @return array tuple containing (ApiReponse, API key)
129
     */
130
    public function request($method, $url, $params = null, $headers = null, $apiMode = 'v1', $usage = [])
23✔
131
    {
132
        $params = $params ?: [];
23✔
133
        $headers = $headers ?: [];
23✔
134
        list($rbody, $rcode, $rheaders, $myApiKey) =
23✔
135
            $this->_requestRaw($method, $url, $params, $headers, $apiMode, $usage);
23✔
136
        $json = $this->_interpretResponse($rbody, $rcode, $rheaders, $apiMode);
22✔
137
        $resp = new ApiResponse($rbody, $rcode, $rheaders, $json);
6✔
138

139
        return [$resp, $myApiKey];
6✔
140
    }
141

142
    /**
143
     * @param 'delete'|'get'|'post' $method
144
     * @param string     $url
145
     * @param callable $readBodyChunkCallable
146
     * @param null|array $params
147
     * @param null|array $headers
148
     * @param 'v1'|'v2' $apiMode
149
     * @param string[] $usage
150
     *
151
     * @throws Exception\ApiErrorException
152
     */
NEW
153
    public function requestStream($method, $url, $readBodyChunkCallable, $params = null, $headers = null, $apiMode = 'v1', $usage = [])
×
154
    {
155
        $params = $params ?: [];
×
156
        $headers = $headers ?: [];
×
157
        list($rbody, $rcode, $rheaders, $myApiKey) =
×
NEW
158
            $this->_requestRawStreaming($method, $url, $params, $headers, $apiMode, $usage, $readBodyChunkCallable);
×
159
        if ($rcode >= 300) {
×
NEW
160
            $this->_interpretResponse($rbody, $rcode, $rheaders, $apiMode);
×
161
        }
162
    }
163

164
    /**
165
     * @param string $rbody a JSON string
166
     * @param int $rcode
167
     * @param array $rheaders
168
     * @param array $resp
169
     * @param 'v1'|'v2' $apiMode
170
     *
171
     * @throws Exception\UnexpectedValueException
172
     * @throws Exception\ApiErrorException
173
     */
174
    public function handleErrorResponse($rbody, $rcode, $rheaders, $resp, $apiMode)
16✔
175
    {
176
        if (!\is_array($resp) || !isset($resp['error'])) {
16✔
177
            $msg = "Invalid response object from API: {$rbody} "
×
178
                . "(HTTP response code was {$rcode})";
×
179

180
            throw new Exception\UnexpectedValueException($msg);
×
181
        }
182

183
        $errorData = $resp['error'];
16✔
184

185
        $error = null;
16✔
186

187
        if (\is_string($errorData)) {
16✔
188
            $error = self::_specificOAuthError($rbody, $rcode, $rheaders, $resp, $errorData);
6✔
189
        }
190
        if (!$error) {
16✔
191
            $error = 'v1' === $apiMode ? self::_specificV1APIError($rbody, $rcode, $rheaders, $resp, $errorData) : self::_specificV2APIError($rbody, $rcode, $rheaders, $resp, $errorData);
10✔
192
        }
193

194
        throw $error;
16✔
195
    }
196

197
    /**
198
     * @static
199
     *
200
     * @param string $rbody
201
     * @param int    $rcode
202
     * @param array  $rheaders
203
     * @param array  $resp
204
     * @param array  $errorData
205
     *
206
     * @return Exception\ApiErrorException
207
     */
208
    private static function _specificV1APIError($rbody, $rcode, $rheaders, $resp, $errorData)
9✔
209
    {
210
        $msg = isset($errorData['message']) ? $errorData['message'] : null;
9✔
211
        $param = isset($errorData['param']) ? $errorData['param'] : null;
9✔
212
        $code = isset($errorData['code']) ? $errorData['code'] : null;
9✔
213
        $type = isset($errorData['type']) ? $errorData['type'] : null;
9✔
214
        $declineCode = isset($errorData['decline_code']) ? $errorData['decline_code'] : null;
9✔
215

216
        switch ($rcode) {
217
            case 400:
9✔
218
                // 'rate_limit' code is deprecated, but left here for backwards compatibility
219
                // for API versions earlier than 2015-09-08
220
                if ('rate_limit' === $code) {
4✔
221
                    return Exception\RateLimitException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code, $param);
1✔
222
                }
223
                if ('idempotency_error' === $type) {
3✔
224
                    return Exception\IdempotencyException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
1✔
225
                }
226

227
            // fall through in generic 400 or 404, returns InvalidRequestException by default
228
            // no break
229
            case 404:
5✔
230
                return Exception\InvalidRequestException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code, $param);
3✔
231

232
            case 401:
4✔
233
                return Exception\AuthenticationException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
1✔
234

235
            case 402:
3✔
236
                return Exception\CardException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code, $declineCode, $param);
1✔
237

238
            case 403:
2✔
239
                return Exception\PermissionException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
1✔
240

241
            case 429:
1✔
242
                return Exception\RateLimitException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code, $param);
1✔
243

244
            default:
245
                return Exception\UnknownApiErrorException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
×
246
        }
247
    }
248

249
    /**
250
     * @static
251
     *
252
     * @param string $rbody
253
     * @param int    $rcode
254
     * @param array  $rheaders
255
     * @param array  $resp
256
     * @param array  $errorData
257
     *
258
     * @return Exception\ApiErrorException
259
     */
260
    private static function _specificV2APIError($rbody, $rcode, $rheaders, $resp, $errorData)
2✔
261
    {
262
        $msg = isset($errorData['message']) ? $errorData['message'] : null;
2✔
263
        $code = isset($errorData['code']) ? $errorData['code'] : null;
2✔
264
        $type = isset($errorData['type']) ? $errorData['type'] : null;
2✔
265

266
        switch ($type) {
267
            case 'idempotency_error':
2✔
NEW
268
                return Exception\IdempotencyException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
×
269
            // The beginning of the section generated from our OpenAPI spec
270
            case 'temporary_session_expired':
2✔
271
                return Exception\TemporarySessionExpiredException::factory(
1✔
272
                    $msg,
1✔
273
                    $rcode,
1✔
274
                    $rbody,
1✔
275
                    $resp,
1✔
276
                    $rheaders,
1✔
277
                    $code
1✔
278
                );
1✔
279

280
            // The end of the section generated from our OpenAPI spec
281
            default:
282
                return self::_specificV1APIError($rbody, $rcode, $rheaders, $resp, $errorData);
1✔
283
        }
284
    }
285

286
    /**
287
     * @static
288
     *
289
     * @param bool|string $rbody
290
     * @param int         $rcode
291
     * @param array       $rheaders
292
     * @param array       $resp
293
     * @param string      $errorCode
294
     *
295
     * @return Exception\OAuth\OAuthErrorException
296
     */
297
    private static function _specificOAuthError($rbody, $rcode, $rheaders, $resp, $errorCode)
6✔
298
    {
299
        $description = isset($resp['error_description']) ? $resp['error_description'] : $errorCode;
6✔
300

301
        switch ($errorCode) {
302
            case 'invalid_client':
6✔
303
                return Exception\OAuth\InvalidClientException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
304

305
            case 'invalid_grant':
5✔
306
                return Exception\OAuth\InvalidGrantException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
307

308
            case 'invalid_request':
4✔
309
                return Exception\OAuth\InvalidRequestException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
310

311
            case 'invalid_scope':
3✔
312
                return Exception\OAuth\InvalidScopeException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
313

314
            case 'unsupported_grant_type':
2✔
315
                return Exception\OAuth\UnsupportedGrantTypeException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
316

317
            case 'unsupported_response_type':
1✔
318
                return Exception\OAuth\UnsupportedResponseTypeException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
319

320
            default:
321
                return Exception\OAuth\UnknownOAuthErrorException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
×
322
        }
323
    }
324

325
    /**
326
     * @static
327
     *
328
     * @param null|array $appInfo
329
     *
330
     * @return null|string
331
     */
332
    private static function _formatAppInfo($appInfo)
23✔
333
    {
334
        if (null !== $appInfo) {
23✔
335
            $string = $appInfo['name'];
23✔
336
            if (\array_key_exists('version', $appInfo) && null !== $appInfo['version']) {
23✔
337
                $string .= '/' . $appInfo['version'];
23✔
338
            }
339
            if (\array_key_exists('url', $appInfo) && null !== $appInfo['url']) {
23✔
340
                $string .= ' (' . $appInfo['url'] . ')';
23✔
341
            }
342

343
            return $string;
23✔
344
        }
345

346
        return null;
×
347
    }
348

349
    /**
350
     * @static
351
     *
352
     * @param string $disableFunctionsOutput - String value of the 'disable_function' setting, as output by \ini_get('disable_functions')
353
     * @param string $functionName - Name of the function we are interesting in seeing whether or not it is disabled
354
     *
355
     * @return bool
356
     */
357
    private static function _isDisabled($disableFunctionsOutput, $functionName)
24✔
358
    {
359
        $disabledFunctions = \explode(',', $disableFunctionsOutput);
24✔
360
        foreach ($disabledFunctions as $disabledFunction) {
24✔
361
            if (\trim($disabledFunction) === $functionName) {
24✔
362
                return true;
1✔
363
            }
364
        }
365

366
        return false;
24✔
367
    }
368

369
    /**
370
     * @static
371
     *
372
     * @param string     $apiKey the Stripe API key, to be used in regular API requests
373
     * @param null       $clientInfo client user agent information
374
     * @param null       $appInfo information to identify a plugin that integrates Stripe using this library
375
     * @param 'v1'|'v2' $apiMode
376
     *
377
     * @return array
378
     */
379
    private static function _defaultHeaders($apiKey, $clientInfo = null, $appInfo = null, $apiMode = 'v1')
23✔
380
    {
381
        $uaString = "Stripe/{$apiMode} PhpBindings/" . Stripe::VERSION;
23✔
382

383
        $langVersion = \PHP_VERSION;
23✔
384
        $uname_disabled = self::_isDisabled(\ini_get('disable_functions'), 'php_uname');
23✔
385
        $uname = $uname_disabled ? '(disabled)' : \php_uname();
23✔
386

387
        // Fallback to global configuration to maintain backwards compatibility.
388
        $appInfo = $appInfo ?: Stripe::getAppInfo();
23✔
389
        $ua = [
23✔
390
            'bindings_version' => Stripe::VERSION,
23✔
391
            'lang' => 'php',
23✔
392
            'lang_version' => $langVersion,
23✔
393
            'publisher' => 'stripe',
23✔
394
            'uname' => $uname,
23✔
395
        ];
23✔
396
        if ($clientInfo) {
23✔
397
            $ua = \array_merge($clientInfo, $ua);
1✔
398
        }
399
        if (null !== $appInfo) {
23✔
400
            $uaString .= ' ' . self::_formatAppInfo($appInfo);
23✔
401
            $ua['application'] = $appInfo;
23✔
402
        }
403

404
        return [
23✔
405
            'X-Stripe-Client-User-Agent' => \json_encode($ua),
23✔
406
            'User-Agent' => $uaString,
23✔
407
            'Authorization' => 'Bearer ' . $apiKey,
23✔
408
            'Stripe-Version' => Stripe::getApiVersion(),
23✔
409
        ];
23✔
410
    }
411

412
    /**
413
     * @param 'delete'|'get'|'post' $method
414
     * @param string $url
415
     * @param array $params
416
     * @param array $headers
417
     * @param 'v1'|'v2' $apiMode
418
     */
419
    private function _prepareRequest($method, $url, $params, $headers, $apiMode)
23✔
420
    {
421
        $myApiKey = $this->_apiKey;
23✔
422
        if (!$myApiKey) {
23✔
423
            $myApiKey = Stripe::$apiKey;
20✔
424
        }
425

426
        if (!$myApiKey) {
23✔
427
            $msg = 'No API key provided.  (HINT: set your API key using '
1✔
428
                . '"Stripe::setApiKey(<API-KEY>)".  You can generate API keys from '
1✔
429
                . 'the Stripe web interface.  See https://stripe.com/api for '
1✔
430
                . 'details, or email support@stripe.com if you have any questions.';
1✔
431

432
            throw new Exception\AuthenticationException($msg);
1✔
433
        }
434

435
        // Clients can supply arbitrary additional keys to be included in the
436
        // X-Stripe-Client-User-Agent header via the optional getUserAgentInfo()
437
        // method
438
        $clientUAInfo = null;
22✔
439
        if (\method_exists($this->httpClient(), 'getUserAgentInfo')) {
22✔
440
            $clientUAInfo = $this->httpClient()->getUserAgentInfo();
×
441
        }
442

443
        if ($params && \is_array($params)) {
22✔
444
            $optionKeysInParams = \array_filter(
×
445
                self::$OPTIONS_KEYS,
×
446
                function ($key) use ($params) {
×
447
                    return \array_key_exists($key, $params);
×
448
                }
×
449
            );
×
450
            if (\count($optionKeysInParams) > 0) {
×
451
                $message = \sprintf('Options found in $params: %s. Options should '
×
452
                    . 'be passed in their own array after $params. (HINT: pass an '
×
453
                    . 'empty array to $params if you do not have any.)', \implode(', ', $optionKeysInParams));
×
454
                \trigger_error($message, \E_USER_WARNING);
×
455
            }
456
        }
457

458
        $absUrl = $this->_apiBase . $url;
22✔
459
        if ('v1' === $apiMode) {
22✔
460
            $params = self::_encodeObjects($params);
19✔
461
        }
462
        $defaultHeaders = $this->_defaultHeaders($myApiKey, $clientUAInfo, $this->_appInfo, $apiMode);
22✔
463

464
        if (Stripe::$accountId) {
22✔
465
            $defaultHeaders['Stripe-Account'] = Stripe::$accountId;
1✔
466
        }
467

468
        if (Stripe::$enableTelemetry && null !== self::$requestTelemetry) {
22✔
469
            $defaultHeaders['X-Stripe-Client-Telemetry'] = self::_telemetryJson(self::$requestTelemetry);
×
470
        }
471

472
        $hasFile = false;
22✔
473
        foreach ($params as $k => $v) {
22✔
474
            if (\is_resource($v)) {
×
475
                $hasFile = true;
×
476
                $params[$k] = self::_processResourceParam($v);
×
477
            } elseif ($v instanceof \CURLFile) {
×
478
                $hasFile = true;
×
479
            }
480
        }
481

482
        if ($hasFile) {
22✔
483
            $defaultHeaders['Content-Type'] = 'multipart/form-data';
×
484
        } elseif ('v2' === $apiMode) {
22✔
485
            $defaultHeaders['Content-Type'] = 'application/json';
3✔
486
        } elseif ('v1' === $apiMode) {
19✔
487
            $defaultHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
19✔
488
        } else {
NEW
489
            throw new Exception\InvalidArgumentException('Unknown API mode: ' . $apiMode);
×
490
        }
491

492
        $combinedHeaders = \array_merge($defaultHeaders, $headers);
22✔
493
        $rawHeaders = [];
22✔
494

495
        foreach ($combinedHeaders as $header => $value) {
22✔
496
            $rawHeaders[] = $header . ': ' . $value;
22✔
497
        }
498

499
        return [$absUrl, $rawHeaders, $params, $hasFile, $myApiKey];
22✔
500
    }
501

502
    /**
503
     * @param 'delete'|'get'|'post' $method
504
     * @param string $url
505
     * @param array $params
506
     * @param array $headers
507
     * @param 'v1'|'v2' $apiMode
508
     * @param string[] $usage
509
     *
510
     * @throws Exception\AuthenticationException
511
     * @throws Exception\ApiConnectionException
512
     *
513
     * @return array
514
     */
515
    private function _requestRaw($method, $url, $params, $headers, $apiMode, $usage)
23✔
516
    {
517
        list($absUrl, $rawHeaders, $params, $hasFile, $myApiKey) = $this->_prepareRequest($method, $url, $params, $headers, $apiMode);
23✔
518

519
        $requestStartMs = Util\Util::currentTimeMillis();
22✔
520

521
        list($rbody, $rcode, $rheaders) = $this->httpClient()->request(
22✔
522
            $method,
22✔
523
            $absUrl,
22✔
524
            $rawHeaders,
22✔
525
            $params,
22✔
526
            $hasFile,
22✔
527
            $apiMode
22✔
528
        );
22✔
529

530
        if (
531
            isset($rheaders['request-id'])
22✔
532
            && \is_string($rheaders['request-id'])
22✔
533
            && '' !== $rheaders['request-id']
22✔
534
        ) {
535
            self::$requestTelemetry = new RequestTelemetry(
×
536
                $rheaders['request-id'],
×
537
                Util\Util::currentTimeMillis() - $requestStartMs,
×
538
                $usage
×
539
            );
×
540
        }
541

542
        return [$rbody, $rcode, $rheaders, $myApiKey];
22✔
543
    }
544

545
    /**
546
     * @param 'delete'|'get'|'post' $method
547
     * @param string $url
548
     * @param array $params
549
     * @param array $headers
550
     * @param string[] $usage
551
     * @param callable $readBodyChunkCallable
552
     * @param 'v1'|'v2' $apiMode
553
     *
554
     * @throws Exception\AuthenticationException
555
     * @throws Exception\ApiConnectionException
556
     *
557
     * @return array
558
     */
NEW
559
    private function _requestRawStreaming($method, $url, $params, $headers, $apiMode, $usage, $readBodyChunkCallable)
×
560
    {
NEW
561
        list($absUrl, $rawHeaders, $params, $hasFile, $myApiKey) = $this->_prepareRequest($method, $url, $params, $headers, $apiMode);
×
562

563
        $requestStartMs = Util\Util::currentTimeMillis();
×
564

565
        list($rbody, $rcode, $rheaders) = $this->streamingHttpClient()->requestStream(
×
566
            $method,
×
567
            $absUrl,
×
568
            $rawHeaders,
×
569
            $params,
×
570
            $hasFile,
×
571
            $readBodyChunkCallable
×
572
        );
×
573

574
        if (
575
            isset($rheaders['request-id'])
×
576
            && \is_string($rheaders['request-id'])
×
577
            && '' !== $rheaders['request-id']
×
578
        ) {
579
            self::$requestTelemetry = new RequestTelemetry(
×
580
                $rheaders['request-id'],
×
581
                Util\Util::currentTimeMillis() - $requestStartMs
×
582
            );
×
583
        }
584

585
        return [$rbody, $rcode, $rheaders, $myApiKey];
×
586
    }
587

588
    /**
589
     * @param resource $resource
590
     *
591
     * @throws Exception\InvalidArgumentException
592
     *
593
     * @return \CURLFile|string
594
     */
595
    private function _processResourceParam($resource)
×
596
    {
597
        if ('stream' !== \get_resource_type($resource)) {
×
598
            throw new Exception\InvalidArgumentException(
×
599
                'Attempted to upload a resource that is not a stream'
×
600
            );
×
601
        }
602

603
        $metaData = \stream_get_meta_data($resource);
×
604
        if ('plainfile' !== $metaData['wrapper_type']) {
×
605
            throw new Exception\InvalidArgumentException(
×
606
                'Only plainfile resource streams are supported'
×
607
            );
×
608
        }
609

610
        // We don't have the filename or mimetype, but the API doesn't care
611
        return new \CURLFile($metaData['uri']);
×
612
    }
613

614
    /**
615
     * @param string $rbody
616
     * @param int    $rcode
617
     * @param array  $rheaders
618
     * @param 'v1'|'v2'  $apiMode
619
     *
620
     * @throws Exception\UnexpectedValueException
621
     * @throws Exception\ApiErrorException
622
     *
623
     * @return array
624
     */
625
    private function _interpretResponse($rbody, $rcode, $rheaders, $apiMode)
22✔
626
    {
627
        $resp = \json_decode($rbody, true);
22✔
628
        $jsonError = \json_last_error();
22✔
629
        if (null === $resp && \JSON_ERROR_NONE !== $jsonError) {
22✔
630
            $msg = "Invalid response body from API: {$rbody} "
×
631
                . "(HTTP response code was {$rcode}, json_last_error() was {$jsonError})";
×
632

633
            throw new Exception\UnexpectedValueException($msg, $rcode);
×
634
        }
635

636
        if ($rcode < 200 || $rcode >= 300) {
22✔
637
            $this->handleErrorResponse($rbody, $rcode, $rheaders, $resp, $apiMode);
16✔
638
        }
639

640
        return $resp;
6✔
641
    }
642

643
    /**
644
     * @static
645
     *
646
     * @param HttpClient\ClientInterface $client
647
     */
648
    public static function setHttpClient($client)
27✔
649
    {
650
        self::$_httpClient = $client;
27✔
651
    }
652

653
    /**
654
     * @static
655
     *
656
     * @param HttpClient\StreamingClientInterface $client
657
     */
658
    public static function setStreamingHttpClient($client)
27✔
659
    {
660
        self::$_streamingHttpClient = $client;
27✔
661
    }
662

663
    /**
664
     * @static
665
     *
666
     * Resets any stateful telemetry data
667
     */
668
    public static function resetTelemetry()
×
669
    {
670
        self::$requestTelemetry = null;
×
671
    }
672

673
    /**
674
     * @return HttpClient\ClientInterface
675
     */
676
    private function httpClient()
23✔
677
    {
678
        if (!self::$_httpClient) {
23✔
679
            self::$_httpClient = HttpClient\CurlClient::instance();
×
680
        }
681

682
        return self::$_httpClient;
23✔
683
    }
684

685
    /**
686
     * @return HttpClient\StreamingClientInterface
687
     */
688
    private function streamingHttpClient()
×
689
    {
690
        if (!self::$_streamingHttpClient) {
×
691
            self::$_streamingHttpClient = HttpClient\CurlClient::instance();
×
692
        }
693

694
        return self::$_streamingHttpClient;
×
695
    }
696
}
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