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

voku / httpful / 24866991492

24 Apr 2026 01:08AM UTC coverage: 89.439% (+0.2%) from 89.228%
24866991492

push

github

web-flow
Merge pull request #23 from voku/copilot/add-helper-methods-debug-analyse-response

Add response debug/analysis helpers: status-range predicates, getErrorMessage(), and debugInfo() with full request context

57 of 59 new or added lines in 1 file covered. (96.61%)

2278 of 2547 relevant lines covered (89.44%)

49.19 hits per line

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

96.97
/src/Httpful/Response.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Httpful;
6

7
use Httpful\Exception\ResponseException;
8
use Psr\Http\Message\RequestInterface;
9
use Psr\Http\Message\ResponseInterface;
10
use Psr\Http\Message\StreamInterface;
11
use voku\helper\UTF8;
12

13
class Response implements ResponseInterface
14
{
15
    /**
16
     * @var StreamInterface
17
     */
18
    private $body;
19

20
    /**
21
     * @var mixed|null
22
     */
23
    private $raw_body;
24

25
    /**
26
     * @var Headers
27
     */
28
    private $headers;
29

30
    /**
31
     * @var mixed|null
32
     */
33
    private $raw_headers;
34

35
    /**
36
     * @var RequestInterface|null
37
     */
38
    private $request;
39

40
    /**
41
     * @var int
42
     */
43
    private $code;
44

45
    /**
46
     * @var string
47
     */
48
    private $reason;
49

50
    /**
51
     * @var string
52
     */
53
    private $content_type = '';
54

55
    /**
56
     * Parent / Generic type (e.g. xml for application/vnd.github.message+xml)
57
     *
58
     * @var string
59
     */
60
    private $parent_type = '';
61

62
    /**
63
     * @var string
64
     */
65
    private $charset = '';
66

67
    /**
68
     * @var array
69
     */
70
    private $meta_data;
71

72
    /**
73
     * @var bool
74
     */
75
    private $is_mime_vendor_specific = false;
76

77
    /**
78
     * @var bool
79
     */
80
    private $is_mime_personal = false;
81

82
    /**
83
     * @param StreamInterface|string|null $body
84
     * @param array|string|null           $headers
85
     * @param RequestInterface|null       $request
86
     * @param array                       $meta_data
87
     *                                               <p>e.g. [protocol_version] = '1.1'</p>
88
     */
89
    public function __construct(
90
        $body = null,
91
        $headers = null,
92
        RequestInterface $request = null,
93
        array $meta_data = []
94
    ) {
95
        if (!($body instanceof Stream)) {
121✔
96
            $this->raw_body = $body;
121✔
97
            $body = Stream::create($body);
121✔
98
        }
99

100
        $this->request = $request;
121✔
101
        $this->raw_headers = $headers;
121✔
102
        $this->meta_data = $meta_data;
121✔
103

104
        if (!isset($this->meta_data['protocol_version'])) {
121✔
105
            $this->meta_data['protocol_version'] = '1.1';
91✔
106
        }
107

108
        if (
109
            \is_string($headers)
121✔
110
            &&
111
            $headers !== ''
121✔
112
        ) {
113
            $this->code = $this->_getResponseCodeFromHeaderString($headers);
80✔
114
            $this->reason = Http::reason($this->code);
80✔
115
            $this->headers = Headers::fromString($headers);
80✔
116
        } elseif (
117
            \is_array($headers)
41✔
118
            &&
119
            \count($headers) > 0
41✔
120
        ) {
121
            $this->code = 200;
9✔
122
            $this->reason = Http::reason($this->code);
9✔
123
            $this->headers = new Headers($headers);
9✔
124
        } else {
125
            $this->code = 200;
32✔
126
            $this->reason = Http::reason($this->code);
32✔
127
            $this->headers = new Headers();
32✔
128
        }
129

130
        $this->_interpretHeaders();
121✔
131

132
        $bodyParsed = $this->_parse($body);
121✔
133
        $this->body = Stream::createNotNull($bodyParsed);
121✔
134
        $this->raw_body = $bodyParsed;
121✔
135
    }
136

137
    /**
138
     * @return void
139
     */
140
    public function __clone()
141
    {
142
        $this->headers = clone $this->headers;
42✔
143
    }
144

145
    /**
146
     * @return string
147
     */
148
    public function __toString()
149
    {
150
        if (
151
            $this->body->getSize() > 0
14✔
152
            &&
153
            !(
14✔
154
                $this->raw_body
14✔
155
                &&
14✔
156
                UTF8::is_serialized((string) $this->body)
14✔
157
            )
14✔
158
        ) {
159
            return (string) $this->body;
10✔
160
        }
161

162
        if (\is_string($this->raw_body)) {
4✔
163
            return (string) $this->raw_body;
×
164
        }
165

166
        return (string) \json_encode($this->raw_body);
4✔
167
    }
168

169
    /**
170
     * @param string $headers
171
     *
172
     * @throws ResponseException if we are unable to parse response code from HTTP response
173
     *
174
     * @return int
175
     *
176
     * @internal
177
     */
178
    public function _getResponseCodeFromHeaderString($headers): int
179
    {
180
        // If there was a redirect, we will get headers from one then one request,
181
        // but will are only interested in the last request.
182
        $headersTmp = \explode("\r\n\r\n", $headers);
80✔
183
        $headersTmpCount = \count($headersTmp);
80✔
184
        if ($headersTmpCount >= 2) {
80✔
185
            $headers = $headersTmp[$headersTmpCount - 2];
64✔
186
        }
187

188
        $end = \strpos($headers, "\r\n");
80✔
189
        if ($end === false) {
80✔
190
            $end = \strlen($headers);
17✔
191
        }
192

193
        $parts = \explode(' ', \substr($headers, 0, $end));
80✔
194

195
        if (
196
            \count($parts) < 2
80✔
197
            ||
198
            !\is_numeric($parts[1])
80✔
199
        ) {
200
            throw new ResponseException('Unable to parse response code from HTTP response due to malformed response: "' . \print_r($headers, true) . '"');
1✔
201
        }
202

203
        return (int) $parts[1];
80✔
204
    }
205

206
    /**
207
     * @return StreamInterface
208
     */
209
    public function getBody(): StreamInterface
210
    {
211
        return $this->body;
15✔
212
    }
213

214
    /**
215
     * Retrieves a message header value by the given case-insensitive name.
216
     *
217
     * This method returns an array of all the header values of the given
218
     * case-insensitive header name.
219
     *
220
     * If the header does not appear in the message, this method MUST return an
221
     * empty array.
222
     *
223
     * @param string $name case-insensitive header field name
224
     *
225
     * @return string[] An array of string values as provided for the given
226
     *                  header. If the header does not appear in the message, this method MUST
227
     *                  return an empty array.
228
     */
229
    public function getHeader($name): array
230
    {
231
        if ($this->headers->offsetExists($name)) {
17✔
232
            $value = $this->headers->offsetGet($name);
17✔
233

234
            if (!\is_array($value)) {
17✔
235
                return [\trim($value, " \t")];
×
236
            }
237

238
            foreach ($value as $keyInner => $valueInner) {
17✔
239
                $value[$keyInner] = \trim($valueInner, " \t");
17✔
240
            }
241

242
            return $value;
17✔
243
        }
244

245
        return [];
2✔
246
    }
247

248
    /**
249
     * Retrieves a comma-separated string of the values for a single header.
250
     *
251
     * This method returns all of the header values of the given
252
     * case-insensitive header name as a string concatenated together using
253
     * a comma.
254
     *
255
     * NOTE: Not all header values may be appropriately represented using
256
     * comma concatenation. For such headers, use getHeader() instead
257
     * and supply your own delimiter when concatenating.
258
     *
259
     * If the header does not appear in the message, this method MUST return
260
     * an empty string.
261
     *
262
     * @param string $name case-insensitive header field name
263
     *
264
     * @return string A string of values as provided for the given header
265
     *                concatenated together using a comma. If the header does not appear in
266
     *                the message, this method MUST return an empty string.
267
     */
268
    public function getHeaderLine($name): string
269
    {
270
        return \implode(', ', $this->getHeader($name));
14✔
271
    }
272

273
    /**
274
     * @return array
275
     */
276
    public function getHeaders(): array
277
    {
278
        return $this->headers->toArray();
19✔
279
    }
280

281
    /**
282
     * Retrieves the HTTP protocol version as a string.
283
     *
284
     * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
285
     *
286
     * @return string HTTP protocol version
287
     */
288
    public function getProtocolVersion(): string
289
    {
290
        if (isset($this->meta_data['protocol_version'])) {
11✔
291
            return (string) $this->meta_data['protocol_version'];
11✔
292
        }
293

294
        return '1.1';
×
295
    }
296

297
    /**
298
     * Gets the response reason phrase associated with the status code.
299
     *
300
     * Because a reason phrase is not a required element in a response
301
     * status line, the reason phrase value MAY be null. Implementations MAY
302
     * choose to return the default RFC 7231 recommended reason phrase (or those
303
     * listed in the IANA HTTP Status Code Registry) for the response's
304
     * status code.
305
     *
306
     * @see http://tools.ietf.org/html/rfc7231#section-6
307
     * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
308
     *
309
     * @return string reason phrase; must return an empty string if none present
310
     */
311
    public function getReasonPhrase(): string
312
    {
313
        return $this->reason;
10✔
314
    }
315

316
    /**
317
     * Gets the response status code.
318
     *
319
     * The status code is a 3-digit integer result code of the server's attempt
320
     * to understand and satisfy the request.
321
     *
322
     * @return int status code
323
     */
324
    public function getStatusCode(): int
325
    {
326
        return $this->code;
25✔
327
    }
328

329
    /**
330
     * Checks if a header exists by the given case-insensitive name.
331
     *
332
     * @param string $name case-insensitive header field name
333
     *
334
     * @return bool Returns true if any header names match the given header
335
     *              name using a case-insensitive string comparison. Returns false if
336
     *              no matching header name is found in the message.
337
     */
338
    public function hasHeader($name): bool
339
    {
340
        return $this->headers->offsetExists($name);
6✔
341
    }
342

343
    /**
344
     * Return an instance with the specified header appended with the given value.
345
     *
346
     * Existing values for the specified header will be maintained. The new
347
     * value(s) will be appended to the existing list. If the header did not
348
     * exist previously, it will be added.
349
     *
350
     * This method MUST be implemented in such a way as to retain the
351
     * immutability of the message, and MUST return an instance that has the
352
     * new header and/or value.
353
     *
354
     * @param string          $name  case-insensitive header field name to add
355
     * @param string|string[] $value header value(s)
356
     *
357
     * @throws \InvalidArgumentException for invalid header names or values
358
     *
359
     * @return static
360
     */
361
    public function withAddedHeader($name, $value): \Psr\Http\Message\MessageInterface
362
    {
363
        $new = clone $this;
4✔
364

365
        if (!\is_array($value)) {
4✔
366
            $value = [$value];
3✔
367
        }
368

369
        if ($new->headers->offsetExists($name)) {
4✔
370
            $new->headers->forceSet($name, \array_merge_recursive($new->headers->offsetGet($name), $value));
3✔
371
        } else {
372
            $new->headers->forceSet($name, $value);
1✔
373
        }
374

375
        return $new;
4✔
376
    }
377

378
    /**
379
     * Return an instance with the specified message body.
380
     *
381
     * The body MUST be a StreamInterface object.
382
     *
383
     * This method MUST be implemented in such a way as to retain the
384
     * immutability of the message, and MUST return a new instance that has the
385
     * new body stream.
386
     *
387
     * @param StreamInterface $body body
388
     *
389
     * @throws \InvalidArgumentException when the body is not valid
390
     *
391
     * @return static
392
     */
393
    public function withBody(StreamInterface $body): \Psr\Http\Message\MessageInterface
394
    {
395
        $new = clone $this;
4✔
396

397
        $new->body = $body;
4✔
398

399
        return $new;
4✔
400
    }
401

402
    /**
403
     * Return an instance with the provided value replacing the specified header.
404
     *
405
     * While header names are case-insensitive, the casing of the header will
406
     * be preserved by this function, and returned from getHeaders().
407
     *
408
     * This method MUST be implemented in such a way as to retain the
409
     * immutability of the message, and MUST return an instance that has the
410
     * new and/or updated header and value.
411
     *
412
     * @param string          $name  case-insensitive header field name
413
     * @param string|string[] $value header value(s)
414
     *
415
     * @throws \InvalidArgumentException for invalid header names or values
416
     *
417
     * @return static
418
     */
419
    public function withHeader($name, $value): \Psr\Http\Message\MessageInterface
420
    {
421
        $new = clone $this;
6✔
422

423
        if (!\is_array($value)) {
6✔
424
            $value = [$value];
5✔
425
        }
426

427
        $new->headers->forceSet($name, $value);
6✔
428

429
        return $new;
6✔
430
    }
431

432
    /**
433
     * Return an instance with the specified HTTP protocol version.
434
     *
435
     * The version string MUST contain only the HTTP version number (e.g.,
436
     * "1.1", "1.0").
437
     *
438
     * This method MUST be implemented in such a way as to retain the
439
     * immutability of the message, and MUST return an instance that has the
440
     * new protocol version.
441
     *
442
     * @param string $version HTTP protocol version
443
     *
444
     * @return static
445
     */
446
    public function withProtocolVersion($version): \Psr\Http\Message\MessageInterface
447
    {
448
        $new = clone $this;
4✔
449

450
        $new->meta_data['protocol_version'] = $version;
4✔
451

452
        return $new;
4✔
453
    }
454

455
    /**
456
     * Return an instance with the specified status code and, optionally, reason phrase.
457
     *
458
     * If no reason phrase is specified, implementations MAY choose to default
459
     * to the RFC 7231 or IANA recommended reason phrase for the response's
460
     * status code.
461
     *
462
     * This method MUST be implemented in such a way as to retain the
463
     * immutability of the message, and MUST return an instance that has the
464
     * updated status and reason phrase.
465
     *
466
     * @see http://tools.ietf.org/html/rfc7231#section-6
467
     * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
468
     *
469
     * @param int    $code         the 3-digit integer result code to set
470
     * @param string $reasonPhrase the reason phrase to use with the
471
     *                             provided status code; if none is provided, implementations MAY
472
     *                             use the defaults as suggested in the HTTP specification
473
     *
474
     * @throws \InvalidArgumentException for invalid status code arguments
475
     *
476
     * @return static
477
     */
478
    public function withStatus($code, $reasonPhrase = null): ResponseInterface
479
    {
480
        $new = clone $this;
20✔
481

482
        $new->code = (int) $code;
20✔
483

484
        if (Http::responseCodeExists($new->code)) {
20✔
485
            $new->reason = Http::reason($new->code);
19✔
486
        } else {
487
            $new->reason = '';
2✔
488
        }
489

490
        if ($reasonPhrase !== null) {
20✔
491
            $new->reason = $reasonPhrase;
4✔
492
        }
493

494
        return $new;
20✔
495
    }
496

497
    /**
498
     * Return an instance without the specified header.
499
     *
500
     * Header resolution MUST be done without case-sensitivity.
501
     *
502
     * This method MUST be implemented in such a way as to retain the
503
     * immutability of the message, and MUST return an instance that removes
504
     * the named header.
505
     *
506
     * @param string $name case-insensitive header field name to remove
507
     *
508
     * @return static
509
     */
510
    public function withoutHeader($name): \Psr\Http\Message\MessageInterface
511
    {
512
        $new = clone $this;
4✔
513

514
        $new->headers->forceUnset($name);
4✔
515

516
        return $new;
4✔
517
    }
518

519
    /**
520
     * @return string
521
     */
522
    public function getCharset(): string
523
    {
524
        return $this->charset;
2✔
525
    }
526

527
    /**
528
     * @return string
529
     */
530
    public function getContentType(): string
531
    {
532
        return $this->content_type;
5✔
533
    }
534

535
    /**
536
     * @return Headers
537
     */
538
    public function getHeadersObject(): Headers
539
    {
540
        return $this->headers;
1✔
541
    }
542

543
    /**
544
     * @return array
545
     */
546
    public function getMetaData(): array
547
    {
548
        return $this->meta_data;
2✔
549
    }
550

551
    /**
552
     * @return string
553
     */
554
    public function getParentType(): string
555
    {
556
        return $this->parent_type;
2✔
557
    }
558

559
    /**
560
     * @return mixed
561
     */
562
    public function getRawBody()
563
    {
564
        return $this->raw_body;
19✔
565
    }
566

567
    /**
568
     * @return string
569
     */
570
    public function getRawHeaders(): string
571
    {
572
        return $this->raw_headers;
2✔
573
    }
574

575
    public function hasBody(): bool
576
    {
577
        return $this->body->getSize()  > 0;
2✔
578
    }
579

580
    /**
581
     * Status Code Definitions.
582
     *
583
     * Informational 1xx
584
     * Successful    2xx
585
     * Redirection   3xx
586
     * Client Error  4xx
587
     * Server Error  5xx
588
     *
589
     * http://pretty-rfc.herokuapp.com/RFC2616#status.codes
590
     *
591
     * @return bool Did we receive a 4xx or 5xx?
592
     */
593
    public function hasErrors(): bool
594
    {
595
        return $this->code >= 400;
8✔
596
    }
597

598
    /**
599
     * @return bool Did we receive a 1xx Informational response?
600
     */
601
    public function isInformational(): bool
602
    {
603
        return $this->code >= 100 && $this->code < 200;
8✔
604
    }
605

606
    /**
607
     * @return bool Did we receive a 2xx Successful response?
608
     */
609
    public function isSuccess(): bool
610
    {
611
        return $this->code >= 200 && $this->code < 300;
16✔
612
    }
613

614
    /**
615
     * @return bool Did we receive a 3xx Redirection response?
616
     */
617
    public function isRedirect(): bool
618
    {
619
        return $this->code >= 300 && $this->code < 400;
9✔
620
    }
621

622
    /**
623
     * @return bool Did we receive a 4xx Client Error response?
624
     */
625
    public function isClientError(): bool
626
    {
627
        return $this->code >= 400 && $this->code < 500;
10✔
628
    }
629

630
    /**
631
     * @return bool Did we receive a 5xx Server Error response?
632
     */
633
    public function isServerError(): bool
634
    {
635
        return $this->code >= 500 && $this->code < 600;
8✔
636
    }
637

638
    /**
639
     * Returns a human-readable hint / explanation for the current HTTP status code.
640
     *
641
     * Useful when displaying or logging error information without having to
642
     * hard-code status-code ranges in calling code.
643
     *
644
     * @return string
645
     */
646
    public function getErrorMessage(): string
647
    {
648
        if ($this->isSuccess()) {
11✔
649
            return 'Request was successful (' . $this->code . ' ' . $this->reason . ').';
5✔
650
        }
651

652
        if ($this->isInformational()) {
6✔
NEW
653
            return 'Informational response (' . $this->code . ' ' . $this->reason . '): the server acknowledged the request.';
×
654
        }
655

656
        if ($this->isRedirect()) {
6✔
657
            return 'Redirect response (' . $this->code . ' ' . $this->reason . '): the resource has moved. Check the Location header.';
1✔
658
        }
659

660
        if ($this->isClientError()) {
5✔
661
            $hints = [
2✔
662
                400 => 'Bad Request: the server could not understand the request due to invalid syntax.',
2✔
663
                401 => 'Unauthorized: authentication is required and has failed or not been provided.',
2✔
664
                403 => 'Forbidden: you do not have permission to access this resource.',
2✔
665
                404 => 'Not Found: the requested resource could not be found on the server.',
2✔
666
                405 => 'Method Not Allowed: the HTTP method used is not supported for this endpoint.',
2✔
667
                408 => 'Request Timeout: the server timed out waiting for the request.',
2✔
668
                409 => 'Conflict: the request conflicts with the current state of the resource.',
2✔
669
                410 => 'Gone: the resource has been permanently removed.',
2✔
670
                422 => 'Unprocessable Entity: the request was well-formed but contains semantic errors.',
2✔
671
                429 => 'Too Many Requests: you have exceeded the rate limit. Try again later.',
2✔
672
            ];
2✔
673

674
            $hint = $hints[$this->code] ?? 'Client Error: the request could not be fulfilled due to a client-side problem. Check your request parameters, headers and authentication.';
2✔
675

676
            return $hint . ' (' . $this->code . ' ' . $this->reason . ')';
2✔
677
        }
678

679
        if ($this->isServerError()) {
3✔
680
            $hints = [
3✔
681
                500 => 'Internal Server Error: the server encountered an unexpected error. This is a server-side problem.',
3✔
682
                501 => 'Not Implemented: the server does not support the functionality required to fulfil the request.',
3✔
683
                502 => 'Bad Gateway: the server received an invalid response from an upstream server.',
3✔
684
                503 => 'Service Unavailable: the server is temporarily unable to handle the request (overloaded or under maintenance).',
3✔
685
                504 => 'Gateway Timeout: the upstream server did not respond in time.',
3✔
686
            ];
3✔
687

688
            $hint = $hints[$this->code] ?? 'Server Error: the server failed to fulfil the request. This is a server-side problem and is not caused by your request.';
3✔
689

690
            return $hint . ' (' . $this->code . ' ' . $this->reason . ')';
3✔
691
        }
692

NEW
693
        return 'Unknown response status (' . $this->code . ' ' . $this->reason . ').';
×
694
    }
695

696
    /**
697
     * Returns a human-readable debug summary that includes the original
698
     * request (method, full URL with query params, request headers, request
699
     * body) as well as the response (status, hint, response headers, response
700
     * body).  Useful for logging, debugging, or building descriptive exception
701
     * messages.
702
     *
703
     * @return string
704
     */
705
    public function debugInfo(): string
706
    {
707
        $lines = [];
5✔
708

709
        // ---- Request section (available when the response was produced by a Request) ----
710
        if ($this->request !== null) {
5✔
711
            $lines[] = '--- Request ---';
2✔
712
            $lines[] = $this->request->getMethod() . ' ' . (string) $this->request->getUri();
2✔
713

714
            foreach ($this->request->getHeaders() as $name => $values) {
2✔
715
                $lines[] = $name . ': ' . \implode(', ', (array) $values);
2✔
716
            }
717

718
            $requestBody = (string) $this->request->getBody();
2✔
719
            if ($requestBody !== '') {
2✔
720
                $lines[] = '';
2✔
721
                $lines[] = $requestBody;
2✔
722
            }
723

724
            $lines[] = '';
2✔
725
        }
726

727
        // ---- Response section ----
728
        $lines[] = '=== Response Debug Info ===';
5✔
729
        $lines[] = 'Status : ' . $this->code . ' ' . $this->reason;
5✔
730
        $lines[] = 'Hint   : ' . $this->getErrorMessage();
5✔
731
        $lines[] = '';
5✔
732
        $lines[] = '--- Response Headers ---';
5✔
733

734
        foreach ($this->headers->toArray() as $name => $values) {
5✔
735
            $lines[] = $name . ': ' . \implode(', ', (array) $values);
2✔
736
        }
737

738
        $lines[] = '';
5✔
739
        $lines[] = '--- Response Body ---';
5✔
740
        $lines[] = (string) $this->body;
5✔
741

742
        return \implode("\n", $lines);
5✔
743
    }
744

745
    /**
746
     * @return bool
747
     */
748
    public function isMimePersonal(): bool
749
    {
750
        return $this->is_mime_personal;
1✔
751
    }
752

753
    /**
754
     * @return bool
755
     */
756
    public function isMimeVendorSpecific(): bool
757
    {
758
        return $this->is_mime_vendor_specific;
2✔
759
    }
760

761
    /**
762
     * @param string[] $header
763
     *
764
     * @return static
765
     */
766
    public function withHeaders(array $header)
767
    {
768
        $new = clone $this;
2✔
769

770
        foreach ($header as $name => $value) {
2✔
771
            $new = $new->withHeader($name, $value);
2✔
772
        }
773

774
        return $new;
2✔
775
    }
776

777
    /**
778
     * After we've parse the headers, let's clean things
779
     * up a bit and treat some headers specially
780
     *
781
     * @return void
782
     */
783
    private function _interpretHeaders()
784
    {
785
        // Parse the Content-Type and charset
786
        $content_type = $this->headers['Content-Type'] ?? [];
121✔
787
        foreach ($content_type as $content_type_inner) {
121✔
788
            $content_type = \array_merge(\explode(';', $content_type_inner));
58✔
789
        }
790

791
        $this->content_type = $content_type[0] ?? '';
121✔
792
        if (
793
            \count($content_type) === 2
121✔
794
            &&
795
            \strpos($content_type[1], '=') !== false
121✔
796
        ) {
797
            /** @noinspection PhpUnusedLocalVariableInspection */
798
            list($nill, $this->charset) = \explode('=', $content_type[1]);
26✔
799
        }
800

801
        // fallback
802
        if (!$this->charset) {
121✔
803
            $this->charset = 'utf-8';
97✔
804
        }
805

806
        // check for vendor & personal type
807
        if (\strpos($this->content_type, '/') !== false) {
121✔
808
            /** @noinspection PhpUnusedLocalVariableInspection */
809
            list($type, $sub_type) = \explode('/', $this->content_type);
58✔
810
            $this->is_mime_vendor_specific = \strpos($sub_type, 'vnd.') === 0;
58✔
811
            $this->is_mime_personal = \strpos($sub_type, 'prs.') === 0;
58✔
812
        }
813

814
        $this->parent_type = $this->content_type;
121✔
815
        if (\strpos($this->content_type, '+') !== false) {
121✔
816
            /** @noinspection PhpUnusedLocalVariableInspection */
817
            list($vendor, $this->parent_type) = \explode('+', $this->content_type, 2);
4✔
818
            $this->parent_type = Mime::getFullMime($this->parent_type);
4✔
819
        }
820
    }
821

822
    /**
823
     * Parse the response into a clean data structure
824
     * (most often an associative array) based on the expected
825
     * Mime type.
826
     *
827
     * @param StreamInterface|null $body Http response body
828
     *
829
     * @return mixed the response parse accordingly
830
     */
831
    private function _parse($body)
832
    {
833
        // If the user decided to forgo the automatic smart parsing, short circuit.
834
        if (
835
            $this->request instanceof Request
121✔
836
            &&
837
            !$this->request->isAutoParse()
121✔
838
        ) {
839
            return $body;
1✔
840
        }
841

842
        // If provided, use custom parsing callback.
843
        if (
844
            $this->request instanceof Request
121✔
845
            &&
846
            $this->request->hasParseCallback()
121✔
847
        ) {
848
            return \call_user_func($this->request->getParseCallback(), $body);
×
849
        }
850

851
        // Decide how to parse the body of the response in the following order:
852
        //
853
        //  1. If provided, use the mime type specifically set as part of the `Request`
854
        //  2. If a MimeHandler is registered for the content type, use it
855
        //  3. If provided, use the "parent type" of the mime type from the response
856
        //  4. Default to the content-type provided in the response
857
        if ($this->request instanceof Request) {
121✔
858
            $parse_with = $this->request->getExpectedType();
79✔
859
        }
860

861
        if (empty($parse_with)) {
121✔
862
            if (Setup::hasParserRegistered($this->content_type)) {
42✔
863
                $parse_with = $this->content_type;
1✔
864
            } else {
865
                $parse_with = $this->parent_type;
41✔
866
            }
867
        }
868

869
        return Setup::setupGlobalMimeType($parse_with)->parse((string) $body);
121✔
870
    }
871
}
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