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

voku / httpful / 25169179125

30 Apr 2026 01:50PM UTC coverage: 90.495% (+0.6%) from 89.902%
25169179125

Pull #26

github

web-flow
Merge b1d37be55 into 836887e5c
Pull Request #26: [+]: PHP 8.0+ rework

354 of 390 new or added lines in 7 files covered. (90.77%)

1 existing line in 1 file now uncovered.

2542 of 2809 relevant lines covered (90.49%)

49.27 hits per line

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

99.2
/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<string, mixed>
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, string|string[]>|string|null $headers
85
     * @param RequestInterface|null                       $request
86
     * @param array<string, mixed>                       $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
        $bodyWasStream = $body instanceof StreamInterface;
129✔
96
        if (!$bodyWasStream) {
129✔
97
            $this->raw_body = $body;
128✔
98
        }
99

100
        $body = Stream::createNotNull($body);
129✔
101

102
        $this->request = $request;
129✔
103
        $this->raw_headers = $headers;
129✔
104
        $this->meta_data = $meta_data;
129✔
105

106
        if (!isset($this->meta_data['protocol_version'])) {
129✔
107
            $this->meta_data['protocol_version'] = '1.1';
99✔
108
        }
109

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

132
        $this->_interpretHeaders();
129✔
133

134
        $preserveOriginalBodyStream = $bodyWasStream
129✔
135
            && $this->request === null
129✔
136
            && (
129✔
137
                $this->raw_headers === null
129✔
138
                || $this->raw_headers === ''
129✔
139
                || $this->raw_headers === []
129✔
140
            );
129✔
141

142
        $bodyParsed = $preserveOriginalBodyStream ? $body : $this->_parse($body);
129✔
143
        $this->body = Stream::createNotNull($bodyParsed);
129✔
144
        $this->raw_body = $bodyParsed;
129✔
145
    }
146

147
    /**
148
     * @return void
149
     */
150
    public function __clone()
151
    {
152
        $this->headers = clone $this->headers;
44✔
153
    }
154

155
    /**
156
     * @return string
157
     */
158
    public function __toString()
159
    {
160
        if (
161
            $this->body->getSize() > 0
15✔
162
            &&
163
            !(
15✔
164
                $this->raw_body
15✔
165
                &&
15✔
166
                UTF8::is_serialized((string) $this->body)
15✔
167
            )
15✔
168
        ) {
169
            return (string) $this->body;
10✔
170
        }
171

172
        if (\is_string($this->raw_body)) {
5✔
173
            return (string) $this->raw_body;
1✔
174
        }
175

176
        return (string) \json_encode($this->raw_body);
4✔
177
    }
178

179
    /**
180
     * @param string $headers
181
     *
182
     * @throws ResponseException if we are unable to parse response code from HTTP response
183
     *
184
     * @return int
185
     *
186
     * @internal
187
     */
188
    public function _getResponseCodeFromHeaderString($headers): int
189
    {
190
        // If there was a redirect, we will get headers from one then one request,
191
        // but will are only interested in the last request.
192
        $headersTmp = \explode("\r\n\r\n", $headers);
83✔
193
        $headersTmpCount = \count($headersTmp);
83✔
194
        if ($headersTmpCount >= 2) {
83✔
195
            $headers = $headersTmp[$headersTmpCount - 2];
67✔
196
        }
197

198
        $end = \strpos($headers, "\r\n");
83✔
199
        if ($end === false) {
83✔
200
            $end = \strlen($headers);
19✔
201
        }
202

203
        $parts = \explode(' ', \substr($headers, 0, $end));
83✔
204

205
        if (
206
            \count($parts) < 2
83✔
207
            ||
208
            !\is_numeric($parts[1])
83✔
209
        ) {
210
            throw new ResponseException('Unable to parse response code from HTTP response due to malformed response: "' . \print_r($headers, true) . '"');
1✔
211
        }
212

213
        return (int) $parts[1];
83✔
214
    }
215

216
    /**
217
     * @return StreamInterface
218
     */
219
    public function getBody(): StreamInterface
220
    {
221
        return $this->body;
16✔
222
    }
223

224
    /**
225
     * Retrieves a message header value by the given case-insensitive name.
226
     *
227
     * This method returns an array of all the header values of the given
228
     * case-insensitive header name.
229
     *
230
     * If the header does not appear in the message, this method MUST return an
231
     * empty array.
232
     *
233
     * @param string $name case-insensitive header field name
234
     *
235
     * @return string[] An array of string values as provided for the given
236
     *                  header. If the header does not appear in the message, this method MUST
237
     *                  return an empty array.
238
     */
239
    public function getHeader($name): array
240
    {
241
        if ($this->headers->offsetExists($name)) {
18✔
242
            $value = $this->headers->offsetGet($name);
18✔
243

244
            if (!\is_array($value)) {
18✔
245
                return [\trim($value, " \t")];
1✔
246
            }
247

248
            foreach ($value as $keyInner => $valueInner) {
17✔
249
                $value[$keyInner] = \trim($valueInner, " \t");
17✔
250
            }
251

252
            return $value;
17✔
253
        }
254

255
        return [];
2✔
256
    }
257

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

283
    /**
284
     * @return array<string, string[]>
285
     */
286
    public function getHeaders(): array
287
    {
288
        return $this->headers->toArray();
19✔
289
    }
290

291
    /**
292
     * Retrieves the HTTP protocol version as a string.
293
     *
294
     * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
295
     *
296
     * @return string HTTP protocol version
297
     */
298
    public function getProtocolVersion(): string
299
    {
300
        if (isset($this->meta_data['protocol_version'])) {
14✔
301
            return (string) $this->meta_data['protocol_version'];
13✔
302
        }
303

304
        return '1.1';
1✔
305
    }
306

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

326
    /**
327
     * Gets the response status code.
328
     *
329
     * The status code is a 3-digit integer result code of the server's attempt
330
     * to understand and satisfy the request.
331
     *
332
     * @return int status code
333
     */
334
    public function getStatusCode(): int
335
    {
336
        return $this->code;
25✔
337
    }
338

339
    /**
340
     * Checks if a header exists by the given case-insensitive name.
341
     *
342
     * @param string $name case-insensitive header field name
343
     *
344
     * @return bool Returns true if any header names match the given header
345
     *              name using a case-insensitive string comparison. Returns false if
346
     *              no matching header name is found in the message.
347
     */
348
    public function hasHeader($name): bool
349
    {
350
        return $this->headers->offsetExists($name);
6✔
351
    }
352

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

375
        if (!\is_array($value)) {
4✔
376
            $value = [$value];
3✔
377
        }
378

379
        if ($new->headers->offsetExists($name)) {
4✔
380
            $new->headers->forceSet($name, \array_merge_recursive($new->headers->offsetGet($name), $value));
3✔
381
        } else {
382
            $new->headers->forceSet($name, $value);
1✔
383
        }
384

385
        return $new;
4✔
386
    }
387

388
    /**
389
     * Return an instance with the specified message body.
390
     *
391
     * The body MUST be a StreamInterface object.
392
     *
393
     * This method MUST be implemented in such a way as to retain the
394
     * immutability of the message, and MUST return a new instance that has the
395
     * new body stream.
396
     *
397
     * @param StreamInterface $body body
398
     *
399
     * @throws \InvalidArgumentException when the body is not valid
400
     *
401
     * @return static
402
     */
403
    public function withBody(StreamInterface $body): \Psr\Http\Message\MessageInterface
404
    {
405
        $new = clone $this;
5✔
406

407
        $new->body = $body;
5✔
408

409
        return $new;
5✔
410
    }
411

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

433
        if (!\is_array($value)) {
6✔
434
            $value = [$value];
5✔
435
        }
436

437
        $new->headers->forceSet($name, $value);
6✔
438

439
        return $new;
6✔
440
    }
441

442
    /**
443
     * Return an instance with the specified HTTP protocol version.
444
     *
445
     * The version string MUST contain only the HTTP version number (e.g.,
446
     * "1.1", "1.0").
447
     *
448
     * This method MUST be implemented in such a way as to retain the
449
     * immutability of the message, and MUST return an instance that has the
450
     * new protocol version.
451
     *
452
     * @param string $version HTTP protocol version
453
     *
454
     * @return static
455
     */
456
    public function withProtocolVersion($version): \Psr\Http\Message\MessageInterface
457
    {
458
        $new = clone $this;
4✔
459

460
        $new->meta_data['protocol_version'] = $version;
4✔
461

462
        return $new;
4✔
463
    }
464

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

492
        $new->code = (int) $code;
21✔
493

494
        if (Http::responseCodeExists($new->code)) {
21✔
495
            $new->reason = Http::reason($new->code);
20✔
496
        } else {
497
            $new->reason = '';
3✔
498
        }
499

500
        if ($reasonPhrase !== null) {
21✔
501
            $new->reason = $reasonPhrase;
4✔
502
        }
503

504
        return $new;
21✔
505
    }
506

507
    /**
508
     * Return an instance without the specified header.
509
     *
510
     * Header resolution MUST be done without case-sensitivity.
511
     *
512
     * This method MUST be implemented in such a way as to retain the
513
     * immutability of the message, and MUST return an instance that removes
514
     * the named header.
515
     *
516
     * @param string $name case-insensitive header field name to remove
517
     *
518
     * @return static
519
     */
520
    public function withoutHeader($name): \Psr\Http\Message\MessageInterface
521
    {
522
        $new = clone $this;
4✔
523

524
        $new->headers->forceUnset($name);
4✔
525

526
        return $new;
4✔
527
    }
528

529
    /**
530
     * @return float
531
     */
532
    private function _getTransferFloat(string $key): float
533
    {
534
        $value = $this->getTransferInfoValue($key);
1✔
535

536
        return \is_numeric($value) ? (float) $value : 0.0;
1✔
537
    }
538

539
    /**
540
     * @return int
541
     */
542
    private function _getTransferInt(string $key): int
543
    {
544
        $value = $this->getTransferInfoValue($key);
1✔
545

546
        return \is_numeric($value) ? (int) $value : 0;
1✔
547
    }
548

549
    /**
550
     * @return string|null
551
     */
552
    private function _getTransferString(string $key): ?string
553
    {
554
        $value = $this->getTransferInfoValue($key);
2✔
555
        if (!\is_string($value) || $value === '') {
2✔
556
            return null;
1✔
557
        }
558

559
        return $value;
1✔
560
    }
561

562
    /**
563
     * @param mixed  $http_version
564
     * @param string $fallback
565
     *
566
     * @return string
567
     */
568
    private static function _normalizeTransferHttpVersion($http_version, string $fallback): string
569
    {
570
        if (!\is_int($http_version)) {
2✔
571
            if (\is_string($http_version) && $http_version !== '') {
1✔
572
                return $http_version;
1✔
573
            }
574

575
            return $fallback;
1✔
576
        }
577

578
        $map = [
1✔
579
            \CURL_HTTP_VERSION_NONE => $fallback,
1✔
580
            \CURL_HTTP_VERSION_1_0  => Http::HTTP_1_0,
1✔
581
            \CURL_HTTP_VERSION_1_1  => Http::HTTP_1_1,
1✔
582
            \CURL_HTTP_VERSION_2_0  => Http::HTTP_2_0,
1✔
583
        ];
1✔
584

585
        if (\defined('CURL_HTTP_VERSION_2TLS')) {
1✔
586
            $map[(int) \constant('CURL_HTTP_VERSION_2TLS')] = Http::HTTP_2_0;
1✔
587
        }
588

589
        if (\defined('CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE')) {
1✔
590
            $map[(int) \constant('CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE')] = Http::HTTP_2_0;
1✔
591
        }
592

593
        if (\defined('CURL_HTTP_VERSION_3')) {
1✔
594
            $map[(int) \constant('CURL_HTTP_VERSION_3')] = Http::HTTP_3;
1✔
595
        }
596

597
        if (\defined('CURL_HTTP_VERSION_3ONLY')) {
1✔
598
            $map[(int) \constant('CURL_HTTP_VERSION_3ONLY')] = Http::HTTP_3;
1✔
599
        }
600

601
        return $map[$http_version] ?? $fallback;
1✔
602
    }
603

604
    /**
605
     * @return string
606
     */
607
    public function getCharset(): string
608
    {
609
        return $this->charset;
2✔
610
    }
611

612
    /**
613
     * @return string
614
     */
615
    public function getContentType(): string
616
    {
617
        return $this->content_type;
5✔
618
    }
619

620
    /**
621
     * @return Headers
622
     */
623
    public function getHeadersObject(): Headers
624
    {
625
        return $this->headers;
2✔
626
    }
627

628
    /**
629
     * @return array<string, mixed>
630
     */
631
    public function getMetaData(): array
632
    {
633
        return $this->meta_data;
3✔
634
    }
635

636
    /**
637
     * @return array<string, mixed>
638
     */
639
    public function getTransferInfo(): array
640
    {
641
        return $this->getMetaData();
1✔
642
    }
643

644
    /**
645
     * @param mixed|null $fallback
646
     *
647
     * @return mixed|null
648
     */
649
    public function getTransferInfoValue(string $key, $fallback = null)
650
    {
651
        return $this->meta_data[$key] ?? $fallback;
2✔
652
    }
653

654
    /**
655
     * @return string|null
656
     */
657
    public function getEffectiveUrl(): ?string
658
    {
659
        return $this->_getTransferString('url');
1✔
660
    }
661

662
    /**
663
     * @return string|null
664
     */
665
    public function getPrimaryIp(): ?string
666
    {
667
        return $this->_getTransferString('primary_ip');
2✔
668
    }
669

670
    /**
671
     * @return string|null
672
     */
673
    public function getLocalIp(): ?string
674
    {
675
        return $this->_getTransferString('local_ip');
1✔
676
    }
677

678
    /**
679
     * @return int
680
     */
681
    public function getRedirectCount(): int
682
    {
683
        return $this->_getTransferInt('redirect_count');
1✔
684
    }
685

686
    /**
687
     * @return float
688
     */
689
    public function getTotalTime(): float
690
    {
691
        return $this->_getTransferFloat('total_time');
1✔
692
    }
693

694
    /**
695
     * @return float
696
     */
697
    public function getConnectTime(): float
698
    {
699
        return $this->_getTransferFloat('connect_time');
1✔
700
    }
701

702
    /**
703
     * @return float
704
     */
705
    public function getTlsHandshakeTime(): float
706
    {
707
        return $this->_getTransferFloat('appconnect_time');
1✔
708
    }
709

710
    /**
711
     * @return string
712
     */
713
    public function getTransferHttpVersion(): string
714
    {
715
        $http_version = $this->getTransferInfoValue('http_version');
2✔
716

717
        return self::_normalizeTransferHttpVersion($http_version, $this->getProtocolVersion());
2✔
718
    }
719

720
    /**
721
     * @return string
722
     */
723
    public function getParentType(): string
724
    {
725
        return $this->parent_type;
2✔
726
    }
727

728
    /**
729
     * @return mixed
730
     */
731
    public function getRawBody()
732
    {
733
        return $this->raw_body;
22✔
734
    }
735

736
    /**
737
     * @return string
738
     */
739
    public function getRawHeaders(): string
740
    {
741
        return $this->raw_headers;
2✔
742
    }
743

744
    public function hasBody(): bool
745
    {
746
        return $this->body->getSize()  > 0;
2✔
747
    }
748

749
    /**
750
     * Status Code Definitions.
751
     *
752
     * Informational 1xx
753
     * Successful    2xx
754
     * Redirection   3xx
755
     * Client Error  4xx
756
     * Server Error  5xx
757
     *
758
     * http://pretty-rfc.herokuapp.com/RFC2616#status.codes
759
     *
760
     * @return bool Did we receive a 4xx or 5xx?
761
     */
762
    public function hasErrors(): bool
763
    {
764
        return $this->code >= 400;
8✔
765
    }
766

767
    /**
768
     * @return bool Did we receive a 1xx Informational response?
769
     */
770
    public function isInformational(): bool
771
    {
772
        return $this->code >= 100 && $this->code < 200;
9✔
773
    }
774

775
    /**
776
     * @return bool Did we receive a 2xx Successful response?
777
     */
778
    public function isSuccess(): bool
779
    {
780
        return $this->code >= 200 && $this->code < 300;
17✔
781
    }
782

783
    /**
784
     * @return bool Did we receive a 3xx Redirection response?
785
     */
786
    public function isRedirect(): bool
787
    {
788
        return $this->code >= 300 && $this->code < 400;
10✔
789
    }
790

791
    /**
792
     * @return bool Did we receive a 4xx Client Error response?
793
     */
794
    public function isClientError(): bool
795
    {
796
        return $this->code >= 400 && $this->code < 500;
11✔
797
    }
798

799
    /**
800
     * @return bool Did we receive a 5xx Server Error response?
801
     */
802
    public function isServerError(): bool
803
    {
804
        return $this->code >= 500 && $this->code < 600;
9✔
805
    }
806

807
    /**
808
     * Returns a human-readable hint / explanation for the current HTTP status code.
809
     *
810
     * Useful when displaying or logging error information without having to
811
     * hard-code status-code ranges in calling code.
812
     *
813
     * @return string
814
     */
815
    public function getErrorMessage(): string
816
    {
817
        if ($this->isSuccess()) {
12✔
818
            return 'Request was successful (' . $this->code . ' ' . $this->reason . ').';
5✔
819
        }
820

821
        if ($this->isInformational()) {
7✔
822
            return 'Informational response (' . $this->code . ' ' . $this->reason . '): the server acknowledged the request.';
1✔
823
        }
824

825
        if ($this->isRedirect()) {
7✔
826
            return 'Redirect response (' . $this->code . ' ' . $this->reason . '): the resource has moved. Check the Location header.';
1✔
827
        }
828

829
        if ($this->isClientError()) {
6✔
830
            $hints = [
2✔
831
                400 => 'Bad Request: the server could not understand the request due to invalid syntax.',
2✔
832
                401 => 'Unauthorized: authentication is required and has failed or not been provided.',
2✔
833
                403 => 'Forbidden: you do not have permission to access this resource.',
2✔
834
                404 => 'Not Found: the requested resource could not be found on the server.',
2✔
835
                405 => 'Method Not Allowed: the HTTP method used is not supported for this endpoint.',
2✔
836
                408 => 'Request Timeout: the server timed out waiting for the request.',
2✔
837
                409 => 'Conflict: the request conflicts with the current state of the resource.',
2✔
838
                410 => 'Gone: the resource has been permanently removed.',
2✔
839
                422 => 'Unprocessable Entity: the request was well-formed but contains semantic errors.',
2✔
840
                429 => 'Too Many Requests: you have exceeded the rate limit. Try again later.',
2✔
841
            ];
2✔
842

843
            $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✔
844

845
            return $hint . ' (' . $this->code . ' ' . $this->reason . ')';
2✔
846
        }
847

848
        if ($this->isServerError()) {
4✔
849
            $hints = [
3✔
850
                500 => 'Internal Server Error: the server encountered an unexpected error. This is a server-side problem.',
3✔
851
                501 => 'Not Implemented: the server does not support the functionality required to fulfil the request.',
3✔
852
                502 => 'Bad Gateway: the server received an invalid response from an upstream server.',
3✔
853
                503 => 'Service Unavailable: the server is temporarily unable to handle the request (overloaded or under maintenance).',
3✔
854
                504 => 'Gateway Timeout: the upstream server did not respond in time.',
3✔
855
            ];
3✔
856

857
            $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✔
858

859
            return $hint . ' (' . $this->code . ' ' . $this->reason . ')';
3✔
860
        }
861

862
        return 'Unknown response status (' . $this->code . ' ' . $this->reason . ').';
1✔
863
    }
864

865
    /**
866
     * Returns a human-readable debug summary that includes the original
867
     * request (method, full URL with query params, request headers, request
868
     * body) as well as the response (status, hint, response headers, response
869
     * body).  Useful for logging, debugging, or building descriptive exception
870
     * messages.
871
     *
872
     * @return string
873
     */
874
    public function debugInfo(): string
875
    {
876
        $lines = [];
5✔
877

878
        // ---- Request section (available when the response was produced by a Request) ----
879
        if ($this->request !== null) {
5✔
880
            $lines[] = '--- Request ---';
2✔
881
            $lines[] = $this->request->getMethod() . ' ' . (string) $this->request->getUri();
2✔
882

883
            foreach ($this->request->getHeaders() as $name => $values) {
2✔
884
                $lines[] = $name . ': ' . \implode(', ', (array) $values);
2✔
885
            }
886

887
            $requestBody = (string) $this->request->getBody();
2✔
888
            if ($requestBody !== '') {
2✔
889
                $lines[] = '';
2✔
890
                $lines[] = $requestBody;
2✔
891
            }
892

893
            $lines[] = '';
2✔
894
        }
895

896
        // ---- Response section ----
897
        $lines[] = '=== Response Debug Info ===';
5✔
898
        $lines[] = 'Status : ' . $this->code . ' ' . $this->reason;
5✔
899
        $lines[] = 'Hint   : ' . $this->getErrorMessage();
5✔
900
        $lines[] = '';
5✔
901
        $lines[] = '--- Response Headers ---';
5✔
902

903
        foreach ($this->headers->toArray() as $name => $values) {
5✔
904
            $lines[] = $name . ': ' . \implode(', ', (array) $values);
2✔
905
        }
906

907
        $lines[] = '';
5✔
908
        $lines[] = '--- Response Body ---';
5✔
909
        $lines[] = (string) $this->body;
5✔
910

911
        return \implode("\n", $lines);
5✔
912
    }
913

914
    /**
915
     * @return bool
916
     */
917
    public function isMimePersonal(): bool
918
    {
919
        return $this->is_mime_personal;
1✔
920
    }
921

922
    /**
923
     * @return bool
924
     */
925
    public function isMimeVendorSpecific(): bool
926
    {
927
        return $this->is_mime_vendor_specific;
2✔
928
    }
929

930
    /**
931
     * @param string[] $header
932
     *
933
     * @return static
934
     */
935
    public function withHeaders(array $header)
936
    {
937
        $new = clone $this;
2✔
938

939
        foreach ($header as $name => $value) {
2✔
940
            $new = $new->withHeader($name, $value);
2✔
941
        }
942

943
        return $new;
2✔
944
    }
945

946
    /**
947
     * After we've parse the headers, let's clean things
948
     * up a bit and treat some headers specially
949
     *
950
     * @return void
951
     */
952
    private function _interpretHeaders()
953
    {
954
        // Parse the Content-Type and charset
955
        $content_type = $this->headers['Content-Type'] ?? [];
129✔
956
        if (!\is_array($content_type)) {
129✔
NEW
957
            $content_type = [$content_type];
×
958
        }
959

960
        $content_type_parts = [];
129✔
961
        foreach ($content_type as $content_type_inner) {
129✔
962
            if (!\is_string($content_type_inner)) {
59✔
NEW
963
                continue;
×
964
            }
965

966
            $content_type_parts = \array_merge($content_type_parts, \explode(';', $content_type_inner));
59✔
967
        }
968

969
        $this->content_type = $content_type_parts[0] ?? '';
129✔
970
        if (
971
            \count($content_type_parts) === 2
129✔
972
            &&
973
            \strpos($content_type_parts[1], '=') !== false
129✔
974
        ) {
975
            /** @noinspection PhpUnusedLocalVariableInspection */
976
            list($nill, $this->charset) = \explode('=', $content_type_parts[1]);
24✔
977
        }
978

979
        // fallback
980
        if (!$this->charset) {
129✔
981
            $this->charset = 'utf-8';
108✔
982
        }
983

984
        // check for vendor & personal type
985
        if (\strpos($this->content_type, '/') !== false) {
129✔
986
            /** @noinspection PhpUnusedLocalVariableInspection */
987
            list($type, $sub_type) = \explode('/', $this->content_type);
59✔
988
            $this->is_mime_vendor_specific = \strpos($sub_type, 'vnd.') === 0;
59✔
989
            $this->is_mime_personal = \strpos($sub_type, 'prs.') === 0;
59✔
990
        }
991

992
        $this->parent_type = $this->content_type;
129✔
993
        if (\strpos($this->content_type, '+') !== false) {
129✔
994
            /** @noinspection PhpUnusedLocalVariableInspection */
995
            list($vendor, $this->parent_type) = \explode('+', $this->content_type, 2);
4✔
996
            $this->parent_type = Mime::getFullMime($this->parent_type);
4✔
997
        }
998
    }
999

1000
    /**
1001
     * Parse the response into a clean data structure
1002
     * (most often an associative array) based on the expected
1003
     * Mime type.
1004
     *
1005
     * @param StreamInterface|null $body Http response body
1006
     *
1007
     * @return mixed the response parse accordingly
1008
     */
1009
    private function _parse($body)
1010
    {
1011
        // If the user decided to forgo the automatic smart parsing, short circuit.
1012
        if (
1013
            $this->request instanceof Request
128✔
1014
            &&
1015
            !$this->request->isAutoParse()
128✔
1016
        ) {
1017
            return $body;
1✔
1018
        }
1019

1020
        // If provided, use custom parsing callback.
1021
        if (
1022
            $this->request instanceof Request
128✔
1023
            &&
1024
            $this->request->hasParseCallback()
128✔
1025
        ) {
1026
            $parseCallback = $this->request->getParseCallback();
1✔
1027
            if ($parseCallback !== null) {
1✔
1028
                return $parseCallback($body);
1✔
1029
            }
1030
        }
1031

1032
        // Decide how to parse the body of the response in the following order:
1033
        //
1034
        //  1. If provided, use the mime type specifically set as part of the `Request`
1035
        //  2. If a MimeHandler is registered for the content type, use it
1036
        //  3. If provided, use the "parent type" of the mime type from the response
1037
        //  4. Default to the content-type provided in the response
1038
        if ($this->request instanceof Request) {
127✔
1039
            $parse_with = $this->request->getExpectedType();
81✔
1040
        }
1041

1042
        if (empty($parse_with)) {
127✔
1043
            if (Setup::hasParserRegistered($this->content_type)) {
46✔
1044
                $parse_with = $this->content_type;
1✔
1045
            } else {
1046
                $parse_with = $this->parent_type;
45✔
1047
            }
1048
        }
1049

1050
        return Setup::setupGlobalMimeType($parse_with)->parse((string) $body);
127✔
1051
    }
1052
}
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