• 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

87.7
/src/Httpful/Request.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Httpful;
6

7
use Httpful\Curl\Curl;
8
use Httpful\Curl\MultiCurl;
9
use Httpful\Exception\ClientErrorException;
10
use Httpful\Exception\NetworkErrorException;
11
use Httpful\Exception\RequestException;
12
use Psr\Http\Message\MessageInterface;
13
use Psr\Http\Message\RequestInterface;
14
use Psr\Http\Message\StreamInterface;
15
use Psr\Http\Message\UriInterface;
16
use Psr\Log\LoggerInterface;
17
use voku\helper\UTF8;
18

19
/**
20
 * @implements \IteratorAggregate<string, mixed>
21
 * @phpstan-consistent-constructor
22
 */
23
class Request implements \IteratorAggregate, RequestInterface
24
{
25
    const MAX_REDIRECTS_DEFAULT = 25;
26

27
    const SERIALIZE_PAYLOAD_ALWAYS = 1;
28

29
    const SERIALIZE_PAYLOAD_NEVER = 0;
30

31
    const SERIALIZE_PAYLOAD_SMART = 2;
32

33
    /**
34
     * "Request"-template object
35
     *
36
     * @var Request|null
37
     */
38
    private $template;
39

40
    /**
41
     * @var array<string, mixed>
42
     */
43
    private $helperData = [];
44

45
    /**
46
     * @var UriInterface|null
47
     */
48
    private $uri;
49

50
    /**
51
     * @var string
52
     */
53
    private $uri_cache;
54

55
    /**
56
     * @var string
57
     */
58
    private $ssl_key = '';
59

60
    /**
61
     * @var string
62
     */
63
    private $ssl_cert = '';
64

65
    /**
66
     * @var string
67
     */
68
    private $ssl_key_type = '';
69

70
    /**
71
     * @var string|null
72
     */
73
    private $ssl_passphrase;
74

75
    /**
76
     * @var float|int|null
77
     */
78
    private $timeout;
79

80
    /**
81
     * @var float|int|null
82
     */
83
    private $connection_timeout;
84

85
    /**
86
     * @var string
87
     */
88
    private $method = Http::GET;
89

90
    /**
91
     * @var Headers
92
     */
93
    private $headers;
94

95
    /**
96
     * @var string
97
     */
98
    private $raw_headers = '';
99

100
    /**
101
     * @var bool
102
     */
103
    private $strict_ssl = false;
104

105
    /**
106
     * @var string
107
     */
108
    private $cache_control = '';
109

110
    /**
111
     * @var string
112
     */
113
    private $content_type = '';
114

115
    /**
116
     * @var string
117
     */
118
    private $content_charset = '';
119

120
    /**
121
     * @var string
122
     *             <p>e.g.: "gzip" or "deflate"</p>
123
     */
124
    private $content_encoding = '';
125

126
    /**
127
     * @var int|null
128
     *               <p>e.g.: 80 or 443</p>
129
     */
130
    private $port;
131

132
    /**
133
     * @var int
134
     */
135
    private $keep_alive = 300;
136

137
    /**
138
     * @var string
139
     */
140
    private $expected_type = '';
141

142
    /**
143
     * @var array<int, mixed>
144
     */
145
    private $additional_curl_opts = [];
146

147
    /**
148
     * @var bool
149
     */
150
    private $auto_parse = true;
151

152
    /**
153
     * @var int
154
     */
155
    private $serialize_payload_method = self::SERIALIZE_PAYLOAD_SMART;
156

157
    /**
158
     * @var string
159
     */
160
    private $username = '';
161

162
    /**
163
     * @var string
164
     */
165
    private $password = '';
166

167
    /**
168
     * @var string|null
169
     */
170
    private $serialized_payload;
171

172
    /**
173
     * @var \CURLFile[]|string|string[]
174
     */
175
    private $payload = [];
176

177
    /**
178
     * @var array<string|int, mixed>
179
     */
180
    private $params = [];
181

182
    /**
183
     * @var callable|null
184
     */
185
    private $parse_callback;
186

187
    /**
188
     * @var callable|LoggerInterface|null
189
     */
190
    private $error_handler;
191

192
    /**
193
     * @var callable[]
194
     */
195
    private $send_callbacks = [];
196

197
    /**
198
     * @var bool
199
     */
200
    private $follow_redirects = false;
201

202
    /**
203
     * @var int
204
     */
205
    private $max_redirects = self::MAX_REDIRECTS_DEFAULT;
206

207
    /**
208
     * @var array<string, callable>
209
     */
210
    private $payload_serializers = [];
211

212
    /**
213
     * Curl Object
214
     */
215
    private ?Curl $curl = null;
216

217
    /**
218
     * MultiCurl Object
219
     */
220
    private ?MultiCurl $curlMulti = null;
221

222
    /**
223
     * @var bool
224
     */
225
    private $debug = false;
226

227
    /**
228
     * @var string
229
     */
230
    private $protocol_version = Http::HTTP_1_1;
231

232
    /**
233
     * @var int|string|null
234
     */
235
    private $curl_http_version;
236

237
    /**
238
     * @var bool
239
     */
240
    private $retry_by_possible_encoding_error = false;
241

242
    /**
243
     * @var int
244
     */
245
    private $retry = 0;
246

247
    /**
248
     * @var float|int|null
249
     */
250
    private $retry_delay;
251

252
    /**
253
     * @var float|int|null
254
     */
255
    private $retry_max_time;
256

257
    /**
258
     * @var bool
259
     */
260
    private $retry_all_errors = false;
261

262
    /**
263
     * @var bool
264
     */
265
    private $retry_connection_refused = false;
266

267
    /**
268
     * @var callable|string|null
269
     */
270
    private $file_path_for_download;
271

272
    /**
273
     * The Client::get, Client::post, ... syntax is preferred as it is more readable.
274
     *
275
     * @param string|null  $method   Http Method
276
     * @param string|null  $mime     Mime Type to Use
277
     * @param Request|null $template "Request"-template object
278
     */
279
    public function __construct(
280
        ?string $method = null,
281
        ?string $mime = null,
282
        ?self $template = null
283
    ) {
284
        $this->initialize();
381✔
285

286
        $this->template = $template;
381✔
287
        $this->headers = new Headers();
381✔
288

289
        // fallback
290
        if (!isset($this->template)) {
381✔
291
            $this->template = new self(Http::GET, null, $this);
381✔
292
            $this->template = $this->template->disableStrictSSL();
381✔
293
        }
294

295
        $this->_setDefaultsFromTemplate()
381✔
296
            ->_setMethod($method)
381✔
297
            ->_withContentType($mime, Mime::PLAIN)
381✔
298
            ->_withExpectedType($mime, Mime::PLAIN);
381✔
299
    }
300

301
    /**
302
     * Does the heavy lifting.  Uses de facto HTTP
303
     * library cURL to set up the HTTP request.
304
     * Note: It does NOT actually send the request
305
     *
306
     * @throws \Exception
307
     *
308
     * @return static
309
     *
310
     * @internal
311
     */
312
    public function _curlPrep(): self
313
    {
314
        // Check for required stuff.
315
        if ($this->uri === null) {
101✔
316
            throw new RequestException($this, 'Attempting to send a request before defining a URI endpoint.');
×
317
        }
318

319
        // init
320
        $this->initialize();
101✔
321
        $curl = $this->curl;
101✔
322
        if ($curl === null) {
101✔
NEW
323
            throw new NetworkErrorException('Unable to initialize cURL.');
×
324
        }
325

326
        if ($this->params === []) {
101✔
327
            $this->_uriPrep();
100✔
328
        }
329

330
        if ($this->payload === []) {
101✔
331
            $this->serialized_payload = null;
83✔
332
        } else {
333
            $this->serialized_payload = $this->_serializePayload($this->payload);
18✔
334

335
            if (
336
                $this->serialized_payload
18✔
337
                &&
338
                $this->content_charset
18✔
339
                &&
340
                !$this->isUpload()
18✔
341
            ) {
342
                $this->serialized_payload = UTF8::encode(
×
343
                    $this->content_charset,
×
344
                    (string) $this->serialized_payload
×
345
                );
×
346
            }
347
        }
348

349
        if ($this->send_callbacks !== []) {
101✔
350
            foreach ($this->send_callbacks as $callback) {
2✔
351
                /** @noinspection VariableFunctionsUsageInspection */
352
                \call_user_func($callback, $this);
2✔
353
            }
354
        }
355

356
        $curl->setUrl((string) $this->uri);
101✔
357

358
        $ch = $curl->getCurl();
101✔
359
        if ($ch === false) {
101✔
360
            throw new NetworkErrorException('Unable to connect to "' . $this->uri . '". => "curl_init" === false');
×
361
        }
362

363
        $curl->setOpt(\CURLOPT_IPRESOLVE, \CURL_IPRESOLVE_WHATEVER);
101✔
364

365
        if ($this->method === Http::POST) {
101✔
366
            // Use CURLOPT_POST to have browser-like POST-to-GET redirects for 301, 302 and 303
367
            $curl->setOpt(\CURLOPT_POST, true);
15✔
368
        } else {
369
            $curl->setOpt(\CURLOPT_CUSTOMREQUEST, $this->method);
87✔
370
        }
371

372
        if ($this->method === Http::HEAD) {
101✔
373
            $curl->setOpt(\CURLOPT_NOBODY, true);
4✔
374
        }
375

376
        if ($this->hasBasicAuth()) {
101✔
377
            $curl->setOpt(\CURLOPT_USERPWD, $this->username . ':' . $this->password);
7✔
378
        }
379

380
        if ($this->hasClientSideCert()) {
101✔
381
            if (!\file_exists($this->ssl_key)) {
×
382
                throw new RequestException($this, 'Could not read Client Key');
×
383
            }
384

385
            if (!\file_exists($this->ssl_cert)) {
×
386
                throw new RequestException($this, 'Could not read Client Certificate');
×
387
            }
388

NEW
389
            $curl->setOpt(\CURLOPT_SSLCERTTYPE, $this->ssl_key_type);
×
NEW
390
            $curl->setOpt(\CURLOPT_SSLKEYTYPE, $this->ssl_key_type);
×
NEW
391
            $curl->setOpt(\CURLOPT_SSLCERT, $this->ssl_cert);
×
NEW
392
            $curl->setOpt(\CURLOPT_SSLKEY, $this->ssl_key);
×
393
            if ($this->ssl_passphrase !== null) {
×
NEW
394
                $curl->setOpt(\CURLOPT_SSLKEYPASSWD, $this->ssl_passphrase);
×
395
            }
396
        }
397

398
        $curl->setOpt(\CURLOPT_TCP_NODELAY, true);
101✔
399

400
        if ($this->hasTimeout()) {
101✔
401
            $curl->setOpt(\CURLOPT_TIMEOUT_MS, \round($this->timeout * 1000));
4✔
402
        }
403

404
        if ($this->hasConnectionTimeout()) {
101✔
405
            $curl->setOpt(\CURLOPT_CONNECTTIMEOUT_MS, \round($this->connection_timeout * 1000));
3✔
406

407
            if (\DIRECTORY_SEPARATOR !== '\\' && $this->connection_timeout < 1) {
3✔
408
                $curl->setOpt(\CURLOPT_NOSIGNAL, true);
2✔
409
            }
410
        }
411

412
        if ($this->follow_redirects === true) {
101✔
413
            $curl->setOpt(\CURLOPT_FOLLOWLOCATION, true);
25✔
414
            $curl->setOpt(\CURLOPT_MAXREDIRS, $this->max_redirects);
25✔
415
        }
416

417
        $curl->setOpt(\CURLOPT_SSL_VERIFYPEER, $this->strict_ssl);
101✔
418
        // zero is safe for all curl versions
419
        $verifyValue = $this->strict_ssl + 0;
101✔
420
        // support for value 1 removed in cURL 7.28.1 value 2 valid in all versions
421
        if ($verifyValue > 0) {
101✔
422
            ++$verifyValue;
2✔
423
        }
424
        $curl->setOpt(\CURLOPT_SSL_VERIFYHOST, $verifyValue);
101✔
425

426
        $curl->setOpt(\CURLOPT_RETURNTRANSFER, true);
101✔
427

428
        $curl->setOpt(\CURLOPT_ENCODING, $this->content_encoding);
101✔
429

430
        if ($this->port !== null) {
101✔
431
            $curl->setOpt(\CURLOPT_PORT, $this->port);
2✔
432
        }
433

434
        $curl->setOpt(\CURLOPT_PROTOCOLS, \CURLPROTO_HTTP | \CURLPROTO_HTTPS);
101✔
435

436
        $curl->setOpt(\CURLOPT_REDIR_PROTOCOLS, \CURLPROTO_HTTP | \CURLPROTO_HTTPS);
101✔
437

438
        // set Content-Length to the size of the payload if present
439
        if ($this->serialized_payload) {
101✔
440
            $curl->setOpt(\CURLOPT_POSTFIELDS, $this->serialized_payload);
18✔
441

442
            if (!$this->isUpload()) {
18✔
443
                $this->headers->forceSet('Content-Length', $this->_determineLength($this->serialized_payload));
18✔
444
            }
445
        }
446

447
        // init
448
        $headers = [];
101✔
449

450
        // Solve a bug on squid proxy, NONE/411 when miss content length.
451
        if (
452
            !$this->headers->offsetExists('Content-Length')
101✔
453
            &&
454
            !$this->isUpload()
101✔
455
        ) {
456
            $this->headers->forceSet('Content-Length', 0);
83✔
457
        }
458

459
        foreach ($this->headers as $header => $value) {
101✔
460
            foreach ($value as $valueInner) {
101✔
461
                $headers[] = "{$header}: {$valueInner}";
101✔
462
            }
463
        }
464

465
        if ($this->keep_alive) {
101✔
466
            $headers[] = 'Connection: Keep-Alive';
100✔
467
            $headers[] = 'Keep-Alive: ' . $this->keep_alive;
100✔
468
        } else {
469
            $headers[] = 'Connection: close';
1✔
470
        }
471

472
        if (!$this->headers->offsetExists('User-Agent')) {
101✔
473
            $headers[] = $this->buildUserAgent();
99✔
474
        }
475

476
        if ($this->content_charset) {
101✔
477
            $contentType = $this->content_type . '; charset=' . $this->content_charset;
×
478
        } else {
479
            $contentType = $this->content_type;
101✔
480
        }
481
        $headers[] = 'Content-Type: ' . $contentType;
101✔
482

483
        if ($this->cache_control) {
101✔
484
            $headers[] = 'Cache-Control: ' . $this->cache_control;
3✔
485
        }
486

487
        // allow custom Accept header if set
488
        if (!$this->headers->offsetExists('Accept')) {
101✔
489
            // http://pretty-rfc.herokuapp.com/RFC2616#header.accept
490
            $accept = 'Accept: */*; q=0.5, text/plain; q=0.8, text/html;level=3;';
98✔
491

492
            if (!empty($this->expected_type)) {
98✔
493
                $accept .= 'q=0.9, ' . $this->expected_type;
98✔
494
            }
495

496
            $headers[] = $accept;
98✔
497
        }
498

499
        $url = \parse_url((string) $this->uri);
101✔
500

501
        if (\is_array($url) === false) {
101✔
502
            throw new ClientErrorException('Unable to connect to "' . $this->uri . '". => "parse_url" === false');
×
503
        }
504

505
        $path = ($url['path'] ?? '/') . (isset($url['query']) ? '?' . $url['query'] : '');
101✔
506
        $this->raw_headers = "{$this->method} {$path} HTTP/{$this->protocol_version}\r\n";
101✔
507
        $this->raw_headers .= \implode("\r\n", $headers);
101✔
508
        $this->raw_headers .= "\r\n";
101✔
509

510
        // DEBUG
511
        //var_dump($this->_headers->toArray(), $this->_raw_headers);
512

513
        /** @noinspection AlterInForeachInspection */
514
        foreach ($headers as &$header) {
101✔
515
            $pos_tmp = \strpos($header, ': ');
101✔
516
            if (
517
                $pos_tmp !== false
101✔
518
                &&
519
                \strlen($header) - 2 === $pos_tmp
101✔
520
            ) {
521
                // curl requires a special syntax to send empty headers
522
                $header = \substr_replace($header, ';', -2);
1✔
523
            }
524
        }
525
        $curl->setOpt(\CURLOPT_HTTPHEADER, $headers);
101✔
526

527
        if ($this->debug) {
101✔
NEW
528
            $curl->setOpt(\CURLOPT_VERBOSE, true);
×
529
        }
530

531
        // If there are some additional curl opts that the user wants to set, we can tack them in here.
532
        foreach ($this->additional_curl_opts as $curlOpt => $curlVal) {
101✔
533
            $curl->setOpt($curlOpt, $curlVal);
4✔
534
        }
535

536
        $this->_configureRetryBehavior();
101✔
537
        $curl->setOpt(\CURLOPT_HTTP_VERSION, $this->_resolveCurlHttpVersion());
101✔
538

539
        if ($this->file_path_for_download) {
101✔
540
            $curl->download($this->file_path_for_download);
2✔
541
            $curl->setOpt(\CURLOPT_CUSTOMREQUEST, 'GET');
2✔
542
            $curl->setOpt(\CURLOPT_HTTPGET, true);
2✔
543
            $this->disableAutoParsing();
2✔
544
        }
545

546
        return $this;
101✔
547
    }
548

549
    /**
550
     * @return Curl|null
551
     */
552
    public function _curl()
553
    {
554
        return $this->curl;
34✔
555
    }
556

557
    /**
558
     * @return MultiCurl|null
559
     */
560
    public function _curlMulti()
561
    {
562
        return $this->curlMulti;
×
563
    }
564

565
    /**
566
     * Takes care of building the query string to be used in the request URI.
567
     *
568
     * Any existing query string parameters, either passed as part of the URI
569
     * via uri() method, or passed via get() and friends will be preserved,
570
     * with additional parameters (added via params() or param()) appended.
571
     *
572
     * @internal
573
     *
574
     * @return void
575
     */
576
    public function _uriPrep()
577
    {
578
        if ($this->uri === null) {
100✔
579
            throw new ClientErrorException('Unable to connect. => "uri" === null');
×
580
        }
581

582
        $url = \parse_url((string) $this->uri);
100✔
583
        $originalParams = [];
100✔
584

585
        if ($url !== false) {
100✔
586
            if (
587
                isset($url['query'])
100✔
588
                &&
589
                $url['query']
100✔
590
            ) {
591
                \parse_str($url['query'], $originalParams);
9✔
592
            }
593

594
            $params = \array_merge($originalParams, $this->params);
100✔
595
        } else {
596
            $params = $this->params;
×
597
        }
598

599
        $queryString = \http_build_query($params);
100✔
600

601
        if (\strpos((string) $this->uri, '?') !== false) {
100✔
602
            $this->_withUri(
9✔
603
                $this->uri->withQuery(
9✔
604
                    \substr(
9✔
605
                        (string) $this->uri,
9✔
606
                        0,
9✔
607
                        \strpos((string) $this->uri, '?')
9✔
608
                    )
9✔
609
                )
9✔
610
            );
9✔
611
        }
612

613
        if (\count($params)) {
100✔
614
            $this->_withUri($this->uri->withQuery($queryString));
9✔
615
        }
616
    }
617

618
    /**
619
     * Callback invoked after payload has been serialized but before the request has been built.
620
     *
621
     * @param callable $callback (Request $request)
622
     *
623
     * @return static
624
     */
625
    public function beforeSend(callable $callback): self
626
    {
627
        $this->send_callbacks[] = $callback;
5✔
628

629
        return $this;
5✔
630
    }
631

632
    /**
633
     * @return string
634
     */
635
    public function buildUserAgent(): string
636
    {
637
        $user_agent = 'User-Agent: Http/PhpClient (cURL/';
101✔
638
        $curl = \curl_version();
101✔
639

640
        if ($curl && isset($curl['version'])) {
101✔
641
            $user_agent .= $curl['version'];
101✔
642
        } else {
643
            $user_agent .= '?.?.?';
×
644
        }
645

646
        $user_agent .= ' PHP/' . \PHP_VERSION . ' (' . \PHP_OS . ')';
101✔
647

648
        if (isset($_SERVER['SERVER_SOFTWARE'])) {
101✔
649
            $tmp = \preg_replace('~PHP/[\d\.]+~U', '', $_SERVER['SERVER_SOFTWARE']);
×
650
            if (\is_string($tmp)) {
×
651
                $user_agent .= ' ' . $tmp;
×
652
            }
653
        } else {
654
            if (isset($_SERVER['TERM_PROGRAM'])) {
101✔
655
                $user_agent .= " {$_SERVER['TERM_PROGRAM']}";
×
656
            }
657

658
            if (isset($_SERVER['TERM_PROGRAM_VERSION'])) {
101✔
659
                $user_agent .= "/{$_SERVER['TERM_PROGRAM_VERSION']}";
×
660
            }
661
        }
662

663
        if (isset($_SERVER['HTTP_USER_AGENT'])) {
101✔
664
            $user_agent .= " {$_SERVER['HTTP_USER_AGENT']}";
×
665
        }
666

667
        $user_agent .= ')';
101✔
668

669
        return $user_agent;
101✔
670
    }
671

672
    /**
673
     * Use Client Side Cert Authentication
674
     *
675
     * @param string      $key          file path to client key
676
     * @param string      $cert         file path to client cert
677
     * @param string|null $passphrase   for client key
678
     * @param string      $ssl_key_type default PEM
679
     *
680
     * @return static
681
     */
682
    public function clientSideCertAuth($cert, $key, $passphrase = null, $ssl_key_type = 'PEM'): self
683
    {
684
        $this->ssl_cert = $cert;
3✔
685
        $this->ssl_key = $key;
3✔
686
        $this->ssl_key_type = $ssl_key_type;
3✔
687
        $this->ssl_passphrase = $passphrase;
3✔
688

689
        return $this;
3✔
690
    }
691

692
    /**
693
     * @see Request::initialize()
694
     *
695
     * @return void
696
     */
697
    public function close()
698
    {
699
        if ($this->curl && $this->hasBeenInitialized()) {
12✔
700
            $this->curl->close();
12✔
701
        }
702

703
        if ($this->curlMulti && $this->hasBeenInitializedMulti()) {
12✔
704
            $this->curlMulti->close();
×
705
        }
706
    }
707

708
    /**
709
     * HTTP Method Get
710
     *
711
     * @param string|UriInterface $uri
712
     * @param string              $file_path
713
     *
714
     * @return static
715
     */
716
    public static function download($uri, $file_path): self
717
    {
718
        if ($uri instanceof UriInterface) {
3✔
719
            $uri = (string) $uri;
×
720
        }
721

722
        /** @var static $request */
723
        $request = new static(Http::GET);
3✔
724
        $request = $request->withUriFromString($uri);
3✔
725
        $request = $request->withDownload($file_path);
3✔
726
        $request = $request->withCacheControl('no-cache');
3✔
727
        $request = $request->withContentEncoding(Encoding::NONE);
3✔
728

729
        /** @var static $request */
730
        return $request;
3✔
731
    }
732

733
    /**
734
     * HTTP Method Delete
735
     *
736
     * @param string|UriInterface $uri
737
     * @param array<string|int, mixed>|null $params
738
     * @param string|null         $mime
739
     *
740
     * @return static
741
     */
742
    public static function delete($uri, ?array $params = null, ?string $mime = null): self
743
    {
744
        if ($uri instanceof UriInterface) {
7✔
745
            $uri = (string) $uri;
×
746
        }
747

748
        $paramsString = '';
7✔
749
        if ($params !== null) {
7✔
750
            $paramsString = \http_build_query(
3✔
751
                $params,
3✔
752
                '',
3✔
753
                '&',
3✔
754
                \PHP_QUERY_RFC3986
3✔
755
            );
3✔
756
            if ($paramsString) {
3✔
757
                $paramsString = (\strpos($uri, '?') !== false ? '&' : '?') . $paramsString;
3✔
758
            }
759
        }
760

761
        /** @var static $request */
762
        $request = new static(Http::DELETE);
7✔
763
        $request = $request->withUriFromString($uri . $paramsString);
7✔
764

765
        return $request->withMimeType($mime);
7✔
766
    }
767

768
    /**
769
     * @return static
770
     *
771
     * @see Request::enableAutoParsing()
772
     */
773
    public function disableAutoParsing(): self
774
    {
775
        return $this->_autoParse(false);
7✔
776
    }
777

778
    /**
779
     * @return static
780
     *
781
     * @see Request::enableKeepAlive()
782
     */
783
    public function disableKeepAlive(): self
784
    {
785
        $this->keep_alive = 0;
2✔
786

787
        return $this;
2✔
788
    }
789

790
    /**
791
     * @return static
792
     */
793
    public function disableRetryByPossibleEncodingError(): self
794
    {
795
        $this->retry_by_possible_encoding_error = false;
2✔
796

797
        return $this;
2✔
798
    }
799

800
    /**
801
     * @return static
802
     *
803
     * @see Request::enableStrictSSL()
804
     */
805
    public function disableStrictSSL(): self
806
    {
807
        return $this->_strictSSL(false);
381✔
808
    }
809

810
    /**
811
     * @return static
812
     *
813
     * @see Request::followRedirects()
814
     */
815
    public function doNotFollowRedirects(): self
816
    {
817
        return $this->followRedirects(false);
3✔
818
    }
819

820
    /**
821
     * @return static
822
     *
823
     * @see Request::disableAutoParsing()
824
     */
825
    public function enableAutoParsing(): self
826
    {
827
        return $this->_autoParse(true);
3✔
828
    }
829

830
    /**
831
     * @param int $seconds
832
     *
833
     * @return static
834
     *
835
     * @see Request::disableKeepAlive()
836
     */
837
    public function enableKeepAlive(int $seconds = 300): self
838
    {
839
        if ($seconds <= 0) {
6✔
840
            throw new \InvalidArgumentException(
3✔
841
                'Invalid keep-alive input: ' . \var_export($seconds, true)
3✔
842
            );
3✔
843
        }
844

845
        $this->keep_alive = $seconds;
3✔
846

847
        return $this;
3✔
848
    }
849

850
    /**
851
     * @return static
852
     */
853
    public function enableRetryByPossibleEncodingError(): self
854
    {
855
        $this->retry_by_possible_encoding_error = true;
2✔
856

857
        return $this;
2✔
858
    }
859

860
    /**
861
     * @return static
862
     *
863
     * @see Request::disableStrictSSL()
864
     */
865
    public function enableStrictSSL(): self
866
    {
867
        return $this->_strictSSL(true);
7✔
868
    }
869

870
    /**
871
     * @return static
872
     */
873
    public function expectsCsv(): self
874
    {
875
        return $this->withExpectedType(Mime::CSV);
2✔
876
    }
877

878
    /**
879
     * @return static
880
     */
881
    public function expectsForm(): self
882
    {
883
        return $this->withExpectedType(Mime::FORM);
2✔
884
    }
885

886
    /**
887
     * @return static
888
     */
889
    public function expectsHtml(): self
890
    {
891
        return $this->withExpectedType(Mime::HTML);
3✔
892
    }
893

894
    /**
895
     * @return static
896
     */
897
    public function expectsJavascript(): self
898
    {
899
        return $this->withExpectedType(Mime::JS);
2✔
900
    }
901

902
    /**
903
     * @return static
904
     */
905
    public function expectsJs(): self
906
    {
907
        return $this->withExpectedType(Mime::JS);
2✔
908
    }
909

910
    /**
911
     * @return static
912
     */
913
    public function expectsJson(): self
914
    {
915
        return $this->withExpectedType(Mime::JSON);
3✔
916
    }
917

918
    /**
919
     * @return static
920
     */
921
    public function expectsPlain(): self
922
    {
923
        return $this->withExpectedType(Mime::PLAIN);
2✔
924
    }
925

926
    /**
927
     * @return static
928
     */
929
    public function expectsText(): self
930
    {
931
        return $this->withExpectedType(Mime::PLAIN);
2✔
932
    }
933

934
    /**
935
     * @return static
936
     */
937
    public function expectsUpload(): self
938
    {
939
        return $this->withExpectedType(Mime::UPLOAD);
2✔
940
    }
941

942
    /**
943
     * @return static
944
     */
945
    public function expectsXhtml(): self
946
    {
947
        return $this->withExpectedType(Mime::XHTML);
2✔
948
    }
949

950
    /**
951
     * @return static
952
     */
953
    public function expectsXml(): self
954
    {
955
        return $this->withExpectedType(Mime::XML);
2✔
956
    }
957

958
    /**
959
     * @return static
960
     */
961
    public function expectsYaml(): self
962
    {
963
        return $this->withExpectedType(Mime::YAML);
1✔
964
    }
965

966
    /**
967
     * If the response is a 301 or 302 redirect, automatically
968
     * send off another request to that location
969
     *
970
     * @param bool $follow follow or not to follow or maximal number of redirects
971
     *
972
     * @return static
973
     */
974
    public function followRedirects(bool|int $follow = true): self
975
    {
976
        $new = clone $this;
34✔
977

978
        if ($follow === false) {
34✔
979
            $new->max_redirects = 0;
3✔
980
            $new->follow_redirects = false;
3✔
981

982
            return $new;
3✔
983
        }
984

985
        if ($follow === true) {
31✔
986
            $new->max_redirects = static::MAX_REDIRECTS_DEFAULT;
31✔
987
        } else {
UNCOV
988
            $new->max_redirects = \max(0, $follow);
×
989
        }
990

991
        $new->follow_redirects = true;
31✔
992

993
        return $new;
31✔
994
    }
995

996
    /**
997
     * HTTP Method Get
998
     *
999
     * @param string|UriInterface $uri
1000
     * @param array<string|int, mixed>|null $params
1001
     * @param string              $mime
1002
     *
1003
     * @return static
1004
     */
1005
    public static function get($uri, ?array $params = null, ?string $mime = null): self
1006
    {
1007
        if ($uri instanceof UriInterface) {
236✔
1008
            $uri = (string) $uri;
×
1009
        }
1010

1011
        $paramsString = '';
236✔
1012
        if ($params !== null) {
236✔
1013
            $paramsString = \http_build_query(
4✔
1014
                $params,
4✔
1015
                '',
4✔
1016
                '&',
4✔
1017
                \PHP_QUERY_RFC3986
4✔
1018
            );
4✔
1019
            if ($paramsString) {
4✔
1020
                $paramsString = (\strpos($uri, '?') !== false ? '&' : '?') . $paramsString;
4✔
1021
            }
1022
        }
1023

1024
        /** @var static $request */
1025
        $request = new static(Http::GET);
236✔
1026
        $request = $request->withUriFromString($uri . $paramsString);
236✔
1027

1028
        return $request->withMimeType($mime);
236✔
1029
    }
1030

1031
    /**
1032
     * Gets the body of the message.
1033
     *
1034
     * @return StreamInterface returns the body as a stream
1035
     */
1036
    public function getBody(): StreamInterface
1037
    {
1038
        return Http::stream($this->payload);
10✔
1039
    }
1040

1041
    /**
1042
     * Retrieves a message header value by the given case-insensitive name.
1043
     *
1044
     * This method returns an array of all the header values of the given
1045
     * case-insensitive header name.
1046
     *
1047
     * If the header does not appear in the message, this method MUST return an
1048
     * empty array.
1049
     *
1050
     * @param string $name case-insensitive header field name
1051
     *
1052
     * @return string[] An array of string values as provided for the given
1053
     *                  header. If the header does not appear in the message, this method MUST
1054
     *                  return an empty array.
1055
     */
1056
    public function getHeader($name): array
1057
    {
1058
        if ($this->headers->offsetExists($name)) {
21✔
1059
            $value = $this->headers->offsetGet($name);
20✔
1060

1061
            if (!\is_array($value)) {
20✔
1062
                return [\trim($value, " \t")];
×
1063
            }
1064

1065
            foreach ($value as $keyInner => $valueInner) {
20✔
1066
                $value[$keyInner] = \trim($valueInner, " \t");
20✔
1067
            }
1068

1069
            return $value;
20✔
1070
        }
1071

1072
        return [];
2✔
1073
    }
1074

1075
    /**
1076
     * Retrieves a comma-separated string of the values for a single header.
1077
     *
1078
     * This method returns all of the header values of the given
1079
     * case-insensitive header name as a string concatenated together using
1080
     * a comma.
1081
     *
1082
     * NOTE: Not all header values may be appropriately represented using
1083
     * comma concatenation. For such headers, use getHeader() instead
1084
     * and supply your own delimiter when concatenating.
1085
     *
1086
     * If the header does not appear in the message, this method MUST return
1087
     * an empty string.
1088
     *
1089
     * @param string $name case-insensitive header field name
1090
     *
1091
     * @return string A string of values as provided for the given header
1092
     *                concatenated together using a comma. If the header does not appear in
1093
     *                the message, this method MUST return an empty string.
1094
     */
1095
    public function getHeaderLine($name): string
1096
    {
1097
        return \implode(', ', $this->getHeader($name));
17✔
1098
    }
1099

1100
    /**
1101
     * @return array<string, string[]>
1102
     */
1103
    public function getHeaders(): array
1104
    {
1105
        return $this->headers->toArray();
324✔
1106
    }
1107

1108
    /**
1109
     * Retrieves the HTTP method of the request.
1110
     *
1111
     * @return string returns the request method
1112
     */
1113
    public function getMethod(): string
1114
    {
1115
        return $this->method;
25✔
1116
    }
1117

1118
    /**
1119
     * Retrieves the HTTP protocol version as a string.
1120
     *
1121
     * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
1122
     *
1123
     * @return string HTTP protocol version
1124
     */
1125
    public function getProtocolVersion(): string
1126
    {
1127
        return $this->protocol_version;
2✔
1128
    }
1129

1130
    /**
1131
     * Retrieves the message's request target.
1132
     *
1133
     * Retrieves the message's request-target either as it will appear (for
1134
     * clients), as it appeared at request (for servers), or as it was
1135
     * specified for the instance (see withRequestTarget()).
1136
     *
1137
     * In most cases, this will be the origin-form of the composed URI,
1138
     * unless a value was provided to the concrete implementation (see
1139
     * withRequestTarget() below).
1140
     *
1141
     * If no URI is available, and no request-target has been specifically
1142
     * provided, this method MUST return the string "/".
1143
     *
1144
     * @return string
1145
     */
1146
    public function getRequestTarget(): string
1147
    {
1148
        if ($this->uri === null) {
9✔
1149
            return '/';
1✔
1150
        }
1151

1152
        $target = $this->uri->getPath();
8✔
1153

1154
        if (!$target) {
8✔
1155
            $target = '/';
3✔
1156
        }
1157

1158
        if ($this->uri->getQuery() !== '') {
8✔
1159
            $target .= '?' . $this->uri->getQuery();
4✔
1160
        }
1161

1162
        return $target;
8✔
1163
    }
1164

1165
    /**
1166
     * @return null|Uri|UriInterface
1167
     */
1168
    public function getUriOrNull(): ?UriInterface
1169
    {
1170
        return $this->uri;
3✔
1171
    }
1172

1173
    /**
1174
     * @return Uri|UriInterface
1175
     */
1176
    public function getUri(): UriInterface
1177
    {
1178
        if ($this->uri === null) {
15✔
1179
            throw new RequestException($this, 'URI is not set.');
1✔
1180
        }
1181

1182
        return $this->uri;
14✔
1183
    }
1184

1185
    /**
1186
     * Checks if a header exists by the given case-insensitive name.
1187
     *
1188
     * @param string $name case-insensitive header field name
1189
     *
1190
     * @return bool Returns true if any header names match the given header
1191
     *              name using a case-insensitive string comparison. Returns false if
1192
     *              no matching header name is found in the message.
1193
     */
1194
    public function hasHeader($name): bool
1195
    {
1196
        return $this->headers->offsetExists($name);
3✔
1197
    }
1198

1199
    /**
1200
     * Return an instance with the specified header appended with the given value.
1201
     *
1202
     * Existing values for the specified header will be maintained. The new
1203
     * value(s) will be appended to the existing list. If the header did not
1204
     * exist previously, it will be added.
1205
     *
1206
     * This method MUST be implemented in such a way as to retain the
1207
     * immutability of the message, and MUST return an instance that has the
1208
     * new header and/or value.
1209
     *
1210
     * @param string          $name  case-insensitive header field name to add
1211
     * @param string|string[] $value header value(s)
1212
     *
1213
     * @throws \InvalidArgumentException for invalid header names or values
1214
     *
1215
     * @return static
1216
     */
1217
    public function withAddedHeader($name, $value): MessageInterface
1218
    {
1219
        if ($name === '') {
10✔
1220
            throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.');
1✔
1221
        }
1222

1223
        $new = clone $this;
9✔
1224

1225
        if (!\is_array($value)) {
9✔
1226
            $value = [$value];
9✔
1227
        }
1228

1229
        if ($new->headers->offsetExists($name)) {
9✔
1230
            $new->headers->forceSet($name, \array_merge_recursive($new->headers->offsetGet($name), $value));
4✔
1231
        } else {
1232
            $new->headers->forceSet($name, $value);
7✔
1233
        }
1234

1235
        return $new;
9✔
1236
    }
1237

1238
    /**
1239
     * Return an instance with the specified message body.
1240
     *
1241
     * The body MUST be a StreamInterface object.
1242
     *
1243
     * This method MUST be implemented in such a way as to retain the
1244
     * immutability of the message, and MUST return a new instance that has the
1245
     * new body stream.
1246
     *
1247
     * @param StreamInterface $body
1248
     *
1249
     * @throws \InvalidArgumentException when the body is not valid
1250
     *
1251
     * @return static
1252
     */
1253
    public function withBody(StreamInterface $body): MessageInterface
1254
    {
1255
        $stream = Http::stream($body);
3✔
1256

1257
        return (clone $this)->_setBody($stream, null);
3✔
1258
    }
1259

1260
    /**
1261
     * Return an instance with the provided value replacing the specified header.
1262
     *
1263
     * While header names are case-insensitive, the casing of the header will
1264
     * be preserved by this function, and returned from getHeaders().
1265
     *
1266
     * This method MUST be implemented in such a way as to retain the
1267
     * immutability of the message, and MUST return an instance that has the
1268
     * new and/or updated header and value.
1269
     *
1270
     * @param string          $name  case-insensitive header field name
1271
     * @param string|string[] $value header value(s)
1272
     *
1273
     * @throws \InvalidArgumentException for invalid header names or values
1274
     *
1275
     * @return static
1276
     */
1277
    public function withHeader($name, $value): self
1278
    {
1279
        $new = clone $this;
25✔
1280

1281
        if (!\is_array($value)) {
25✔
1282
            $value = [$value];
22✔
1283
        }
1284

1285
        $new->headers->forceSet($name, $value);
25✔
1286

1287
        return $new;
24✔
1288
    }
1289

1290
    /**
1291
     * Return an instance with the provided HTTP method.
1292
     *
1293
     * While HTTP method names are typically all uppercase characters, HTTP
1294
     * method names are case-sensitive and thus implementations SHOULD NOT
1295
     * modify the given string.
1296
     *
1297
     * This method MUST be implemented in such a way as to retain the
1298
     * immutability of the message, and MUST return an instance that has the
1299
     * changed request method.
1300
     *
1301
     * @param string $method
1302
     *                       <p>\Httpful\Http::GET, \Httpful\Http::POST, ...</p>
1303
     *
1304
     * @throws \InvalidArgumentException for invalid HTTP methods
1305
     *
1306
     * @return static
1307
     */
1308
    public function withMethod($method): RequestInterface
1309
    {
1310
        $new = clone $this;
3✔
1311

1312
        $new->_setMethod($method);
3✔
1313

1314
        return $new;
3✔
1315
    }
1316

1317
    /**
1318
     * Return an instance with the specified HTTP protocol version.
1319
     *
1320
     * The version string MUST contain only the HTTP version number (e.g.,
1321
     * "2, 1.1", "1.0").
1322
     *
1323
     * This method MUST be implemented in such a way as to retain the
1324
     * immutability of the message, and MUST return an instance that has the
1325
     * new protocol version.
1326
     *
1327
     * @param string $version
1328
     *                        <p>Http::HTTP_*</p>
1329
     *
1330
     * @return static
1331
     */
1332
    public function withProtocolVersion($version): MessageInterface
1333
    {
1334
        $new = clone $this;
6✔
1335

1336
        $new->protocol_version = $version;
6✔
1337
        $new->curl_http_version = null;
6✔
1338

1339
        switch ((string) $version) {
6✔
1340
            case Http::HTTP_1_0:
1341
                $new->curl_http_version = \CURL_HTTP_VERSION_1_0;
2✔
1342

1343
                break;
2✔
1344
            case Http::HTTP_1_1:
4✔
1345
                $new->curl_http_version = \CURL_HTTP_VERSION_1_1;
1✔
1346

1347
                break;
1✔
1348
            case Http::HTTP_2_0:
4✔
1349
                $new->curl_http_version = \CURL_HTTP_VERSION_2_0;
2✔
1350

1351
                break;
2✔
1352
            case Http::HTTP_3:
2✔
1353
                $new->curl_http_version = 'CURL_HTTP_VERSION_3';
1✔
1354

1355
                break;
1✔
1356
        }
1357

1358
        return $new;
6✔
1359
    }
1360

1361
    /**
1362
     * @return static
1363
     */
1364
    public function withHttp2Tls(): self
1365
    {
1366
        return $this->_withCurlHttpVersion(Http::HTTP_2_0, 'CURL_HTTP_VERSION_2TLS');
1✔
1367
    }
1368

1369
    /**
1370
     * @return static
1371
     */
1372
    public function withHttp2PriorKnowledge(): self
1373
    {
1374
        return $this->_withCurlHttpVersion(Http::HTTP_2_0, 'CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE');
1✔
1375
    }
1376

1377
    /**
1378
     * @return static
1379
     */
1380
    public function withHttp3(): self
1381
    {
NEW
1382
        return $this->_withCurlHttpVersion(Http::HTTP_3, 'CURL_HTTP_VERSION_3');
×
1383
    }
1384

1385
    /**
1386
     * @return static
1387
     */
1388
    public function withHttp3Only(): self
1389
    {
1390
        return $this->_withCurlHttpVersion(Http::HTTP_3, 'CURL_HTTP_VERSION_3ONLY');
1✔
1391
    }
1392

1393
    /**
1394
     * Return an instance with the specific request-target.
1395
     *
1396
     * If the request needs a non-origin-form request-target — e.g., for
1397
     * specifying an absolute-form, authority-form, or asterisk-form —
1398
     * this method may be used to create an instance with the specified
1399
     * request-target, verbatim.
1400
     *
1401
     * This method MUST be implemented in such a way as to retain the
1402
     * immutability of the message, and MUST return an instance that has the
1403
     * changed request target.
1404
     *
1405
     * @see http://tools.ietf.org/html/rfc7230#section-5.3 (for the various
1406
     *     request-target forms allowed in request messages)
1407
     *
1408
     * @param mixed $requestTarget
1409
     *
1410
     * @return static
1411
     */
1412
    public function withRequestTarget($requestTarget): RequestInterface
1413
    {
1414
        if (\preg_match('#\\s#', $requestTarget)) {
6✔
1415
            throw new \InvalidArgumentException('Invalid request target provided; cannot contain whitespace');
3✔
1416
        }
1417

1418
        $new = clone $this;
3✔
1419

1420
        if ($new->uri !== null) {
3✔
1421
            $new->_withUri($new->uri->withPath($requestTarget));
2✔
1422
        }
1423

1424
        return $new;
3✔
1425
    }
1426

1427
    /**
1428
     * Returns an instance with the provided URI.
1429
     *
1430
     * This method MUST update the Host header of the returned request by
1431
     * default if the URI contains a host component. If the URI does not
1432
     * contain a host component, any pre-existing Host header MUST be carried
1433
     * over to the returned request.
1434
     *
1435
     * You can opt-in to preserving the original state of the Host header by
1436
     * setting `$preserveHost` to `true`. When `$preserveHost` is set to
1437
     * `true`, this method interacts with the Host header in the following ways:
1438
     *
1439
     * - If the Host header is missing or empty, and the new URI contains
1440
     *   a host component, this method MUST update the Host header in the returned
1441
     *   request.
1442
     * - If the Host header is missing or empty, and the new URI does not contain a
1443
     *   host component, this method MUST NOT update the Host header in the returned
1444
     *   request.
1445
     * - If a Host header is present and non-empty, this method MUST NOT update
1446
     *   the Host header in the returned request.
1447
     *
1448
     * This method MUST be implemented in such a way as to retain the
1449
     * immutability of the message, and MUST return an instance that has the
1450
     * new UriInterface instance.
1451
     *
1452
     * @see http://tools.ietf.org/html/rfc3986#section-4.3
1453
     *
1454
     * @param UriInterface $uri          new request URI to use
1455
     * @param bool         $preserveHost preserve the original state of the Host header
1456
     *
1457
     * @return static
1458
     */
1459
    public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface
1460
    {
1461
        return (clone $this)->_withUri($uri, $preserveHost);
335✔
1462
    }
1463

1464
    /**
1465
     * Return an instance without the specified header.
1466
     *
1467
     * Header resolution MUST be done without case-sensitivity.
1468
     *
1469
     * This method MUST be implemented in such a way as to retain the
1470
     * immutability of the message, and MUST return an instance that removes
1471
     * the named header.
1472
     *
1473
     * @param string $name case-insensitive header field name to remove
1474
     *
1475
     * @return static
1476
     */
1477
    public function withoutHeader($name): self
1478
    {
1479
        $new = clone $this;
322✔
1480

1481
        $new->headers->forceUnset($name);
322✔
1482

1483
        return $new;
322✔
1484
    }
1485

1486
    /**
1487
     * @return string
1488
     */
1489
    public function getContentType(): string
1490
    {
1491
        return $this->content_type;
10✔
1492
    }
1493

1494
    /**
1495
     * @return callable|LoggerInterface|null
1496
     */
1497
    public function getErrorHandler()
1498
    {
1499
        return $this->error_handler;
2✔
1500
    }
1501

1502
    /**
1503
     * @return string
1504
     */
1505
    public function getExpectedType(): string
1506
    {
1507
        return $this->expected_type;
89✔
1508
    }
1509

1510
    /**
1511
     * @return string
1512
     */
1513
    public function getHttpMethod(): string
1514
    {
1515
        return $this->method;
4✔
1516
    }
1517

1518
    /**
1519
     * @return \ArrayObject<string, mixed>
1520
     */
1521
    public function getIterator(): \ArrayObject
1522
    {
1523
        // init
1524
        $elements = new \ArrayObject();
381✔
1525

1526
        foreach (\get_object_vars($this) as $f => $v) {
381✔
1527
            $elements[$f] = $v;
381✔
1528
        }
1529

1530
        return $elements;
381✔
1531
    }
1532

1533
    /**
1534
     * @return callable|null
1535
     */
1536
    public function getParseCallback()
1537
    {
1538
        return $this->parse_callback;
3✔
1539
    }
1540

1541
    /**
1542
     * @return array<int|string, \CURLFile|string>
1543
     */
1544
    public function getPayload(): array
1545
    {
1546
        return \is_string($this->payload) ? [$this->payload] : $this->payload;
2✔
1547
    }
1548

1549
    /**
1550
     * @return string
1551
     */
1552
    public function getRawHeaders(): string
1553
    {
1554
        return $this->raw_headers;
8✔
1555
    }
1556

1557
    /**
1558
     * @return callable[]
1559
     */
1560
    public function getSendCallback(): array
1561
    {
1562
        return $this->send_callbacks;
5✔
1563
    }
1564

1565
    /**
1566
     * @return int
1567
     */
1568
    public function getSerializePayloadMethod(): int
1569
    {
1570
        return $this->serialize_payload_method;
3✔
1571
    }
1572

1573
    /**
1574
     * @return mixed|null
1575
     */
1576
    public function getSerializedPayload()
1577
    {
1578
        return $this->serialized_payload;
4✔
1579
    }
1580

1581
    /**
1582
     * @return string
1583
     */
1584
    public function getUriString(): string
1585
    {
1586
        return (string) $this->uri;
10✔
1587
    }
1588

1589
    /**
1590
     * Is this request setup for basic auth?
1591
     *
1592
     * @return bool
1593
     */
1594
    public function hasBasicAuth(): bool
1595
    {
1596
        return $this->password && $this->username;
106✔
1597
    }
1598

1599
    /**
1600
     * @return bool has the internal curl (non-multi) request been initialized?
1601
     */
1602
    public function hasBeenInitialized(): bool
1603
    {
1604
        if (!$this->curl) {
105✔
1605
            return false;
×
1606
        }
1607

1608
        return $this->curl->getCurl() !== false;
105✔
1609
    }
1610

1611
    /**
1612
     * @return bool has the internal curl (multi) request been initialized?
1613
     */
1614
    public function hasBeenInitializedMulti(): bool
1615
    {
1616
        if (!$this->curlMulti) {
1✔
1617
            return false;
1✔
1618
        }
1619

NEW
1620
        return $this->curlMulti->getMultiCurl() !== null;
×
1621
    }
1622

1623
    /**
1624
     * @return bool is this request setup for client side cert?
1625
     */
1626
    public function hasClientSideCert(): bool
1627
    {
1628
        return $this->ssl_cert && $this->ssl_key;
106✔
1629
    }
1630

1631
    /**
1632
     * @return bool does the request have a connection timeout?
1633
     */
1634
    public function hasConnectionTimeout(): bool
1635
    {
1636
        return isset($this->connection_timeout);
104✔
1637
    }
1638

1639
    /**
1640
     * Is this request setup for digest auth?
1641
     *
1642
     * @return bool
1643
     */
1644
    public function hasDigestAuth(): bool
1645
    {
1646
        return $this->password
3✔
1647
               &&
3✔
1648
               $this->username
3✔
1649
               &&
3✔
1650
               $this->additional_curl_opts[\CURLOPT_HTTPAUTH] === \CURLAUTH_DIGEST;
3✔
1651
    }
1652

1653
    /**
1654
     * @return bool
1655
     */
1656
    public function hasParseCallback(): bool
1657
    {
1658
        return $this->parse_callback !== null;
85✔
1659
    }
1660

1661
    /**
1662
     * @return bool is this request setup for using proxy?
1663
     */
1664
    public function hasProxy(): bool
1665
    {
1666
        /**
1667
         *  We must be aware that proxy variables could come from environment also.
1668
         *  In curl extension, http proxy can be specified not only via CURLOPT_PROXY option,
1669
         *  but also by environment variable called http_proxy.
1670
         */
1671
        return (
8✔
1672
            isset($this->additional_curl_opts[\CURLOPT_PROXY])
8✔
1673
            && \is_string($this->additional_curl_opts[\CURLOPT_PROXY])
8✔
1674
        ) || \getenv('http_proxy');
8✔
1675
    }
1676

1677
    /**
1678
     * @return bool does the request have a timeout?
1679
     */
1680
    public function hasTimeout(): bool
1681
    {
1682
        return isset($this->timeout);
106✔
1683
    }
1684

1685
    /**
1686
     * HTTP Method Head
1687
     *
1688
     * @param string|UriInterface $uri
1689
     *
1690
     * @return static
1691
     */
1692
    public static function head($uri): self
1693
    {
1694
        if ($uri instanceof UriInterface) {
8✔
1695
            $uri = (string) $uri;
×
1696
        }
1697

1698
        /** @var static $request */
1699
        $request = new static(Http::HEAD);
8✔
1700
        $request = $request->withUriFromString($uri);
8✔
1701

1702
        return $request->withMimeType(Mime::PLAIN);
8✔
1703
    }
1704

1705
    /**
1706
     * @see Request::close()
1707
     *
1708
     * @return void
1709
     */
1710
    public function initializeMulti()
1711
    {
1712
        if (!$this->curlMulti || $this->hasBeenInitializedMulti()) {
28✔
1713
            $this->curlMulti = new MultiCurl();
28✔
1714
        }
1715
    }
1716

1717
    /**
1718
     * @see Request::close()
1719
     *
1720
     * @return void
1721
     */
1722
    public function initialize()
1723
    {
1724
        if (!$this->curl || !$this->hasBeenInitialized()) {
381✔
1725
            $this->curl = new Curl();
381✔
1726
        }
1727
    }
1728

1729
    /**
1730
     * @return bool
1731
     */
1732
    public function isAutoParse(): bool
1733
    {
1734
        return $this->auto_parse;
86✔
1735
    }
1736

1737
    /**
1738
     * @return bool
1739
     */
1740
    public function isJson(): bool
1741
    {
1742
        return $this->content_type === Mime::JSON;
4✔
1743
    }
1744

1745
    /**
1746
     * @return bool
1747
     */
1748
    public function isStrictSSL(): bool
1749
    {
1750
        return $this->strict_ssl;
6✔
1751
    }
1752

1753
    /**
1754
     * @return bool
1755
     */
1756
    public function isUpload(): bool
1757
    {
1758
        return $this->content_type === Mime::UPLOAD;
381✔
1759
    }
1760

1761
    /**
1762
     * @return static
1763
     *
1764
     * @see Request::serializePayloadMode()
1765
     */
1766
    public function neverSerializePayload(): self
1767
    {
1768
        return $this->serializePayloadMode(static::SERIALIZE_PAYLOAD_NEVER);
9✔
1769
    }
1770

1771
    /**
1772
     * HTTP Method Options
1773
     *
1774
     * @param string|UriInterface $uri
1775
     *
1776
     * @return static
1777
     */
1778
    public static function options($uri): self
1779
    {
1780
        if ($uri instanceof UriInterface) {
4✔
1781
            $uri = (string) $uri;
×
1782
        }
1783

1784
        /** @var static $request */
1785
        $request = new static(Http::OPTIONS);
4✔
1786

1787
        return $request->withUriFromString($uri);
4✔
1788
    }
1789

1790
    /**
1791
     * HTTP Method Patch
1792
     *
1793
     * @param string|UriInterface $uri
1794
     * @param mixed               $payload data to send in body of request
1795
     * @param string              $mime    MIME to use for Content-Type
1796
     *
1797
     * @return static
1798
     */
1799
    public static function patch($uri, $payload = null, ?string $mime = null): self
1800
    {
1801
        if ($uri instanceof UriInterface) {
5✔
1802
            $uri = (string) $uri;
×
1803
        }
1804

1805
        /** @var static $request */
1806
        $request = new static(Http::PATCH);
5✔
1807
        $request = $request->withUriFromString($uri);
5✔
1808

1809
        return $request->_setBody($payload, null, $mime);
5✔
1810
    }
1811

1812
    /**
1813
     * HTTP Method Post
1814
     *
1815
     * @param string|UriInterface $uri
1816
     * @param mixed               $payload data to send in body of request
1817
     * @param string              $mime    MIME to use for Content-Type
1818
     *
1819
     * @return static
1820
     */
1821
    public static function post($uri, $payload = null, ?string $mime = null): self
1822
    {
1823
        if ($uri instanceof UriInterface) {
24✔
1824
            $uri = (string) $uri;
×
1825
        }
1826

1827
        /** @var static $request */
1828
        $request = new static(Http::POST);
24✔
1829
        $request = $request->withUriFromString($uri);
24✔
1830

1831
        return $request->_setBody($payload, null, $mime);
24✔
1832
    }
1833

1834
    /**
1835
     * HTTP Method Put
1836
     *
1837
     * @param string|UriInterface $uri
1838
     * @param mixed               $payload data to send in body of request
1839
     * @param string              $mime    MIME to use for Content-Type
1840
     *
1841
     * @return static
1842
     */
1843
    public static function put($uri, $payload = null, ?string $mime = null): self
1844
    {
1845
        if ($uri instanceof UriInterface) {
4✔
1846
            $uri = (string) $uri;
×
1847
        }
1848

1849
        /** @var static $request */
1850
        $request = new static(Http::PUT);
4✔
1851
        $request = $request->withUriFromString($uri);
4✔
1852

1853
        return $request->_setBody($payload, null, $mime);
4✔
1854
    }
1855

1856
    /**
1857
     * Register a callback that will be used to serialize the payload
1858
     * for a particular mime type.  When using "*" for the mime
1859
     * type, it will use that parser for all responses regardless of the mime
1860
     * type.  If a custom '*' and 'application/json' exist, the custom
1861
     * 'application/json' would take precedence over the '*' callback.
1862
     *
1863
     * @param string   $mime     mime type we're registering
1864
     * @param callable $callback takes one argument, $payload,
1865
     *                           which is the payload that we'll be
1866
     *
1867
     * @return static
1868
     */
1869
    public function registerPayloadSerializer($mime, callable $callback): self
1870
    {
1871
        $new = clone $this;
2✔
1872

1873
        $new->payload_serializers[Mime::getFullMime($mime)] = $callback;
2✔
1874

1875
        return $new;
2✔
1876
    }
1877

1878
    /**
1879
     * @return void
1880
     */
1881
    public function reset()
1882
    {
1883
        $this->headers = new Headers();
1✔
1884

1885
        $this->close();
1✔
1886
        $this->initialize();
1✔
1887
    }
1888

1889
    /**
1890
     * Actually send off the request, and parse the response.
1891
     *
1892
     * @param callable|null $onSuccessCallback
1893
     * @param callable|null $onCompleteCallback
1894
     * @param callable|null $onBeforeSendCallback
1895
     * @param callable|null $onErrorCallback
1896
     *
1897
     * @throws NetworkErrorException when unable to parse or communicate w server
1898
     *
1899
     * @return MultiCurl
1900
     */
1901
    public function initMulti(
1902
        $onSuccessCallback = null,
1903
        $onCompleteCallback = null,
1904
        $onBeforeSendCallback = null,
1905
        $onErrorCallback = null
1906
    ) {
1907
        $this->initializeMulti();
27✔
1908
        \assert($this->curlMulti instanceof MultiCurl);
1909

1910
        if ($onSuccessCallback !== null) {
27✔
1911
            $this->curlMulti->success(
5✔
1912
                static function (Curl $instance) use ($onSuccessCallback) {
5✔
1913
                    if ($instance->request instanceof self) {
3✔
1914
                        $response = $instance->request->_buildResponse($instance->rawResponse, $instance);
3✔
1915
                    } else {
1916
                        $response = $instance->rawResponse;
×
1917
                    }
1918

1919
                    $onSuccessCallback(
3✔
1920
                        $response,
3✔
1921
                        $instance->request,
3✔
1922
                        $instance
3✔
1923
                    );
3✔
1924
                }
5✔
1925
            );
5✔
1926
        }
1927

1928
        if ($onCompleteCallback !== null) {
27✔
1929
            $this->curlMulti->complete(
2✔
1930
                static function (Curl $instance) use ($onCompleteCallback) {
2✔
1931
                    if ($instance->request instanceof self) {
×
1932
                        $response = $instance->request->_buildResponse($instance->rawResponse, $instance);
×
1933
                    } else {
1934
                        $response = $instance->rawResponse;
×
1935
                    }
1936

1937
                    $onCompleteCallback(
×
1938
                        $response,
×
1939
                        $instance->request,
×
1940
                        $instance
×
1941
                    );
×
1942
                }
2✔
1943
            );
2✔
1944
        }
1945

1946
        if ($onBeforeSendCallback !== null) {
27✔
1947
            $this->curlMulti->beforeSend(
×
1948
                static function (Curl $instance) use ($onBeforeSendCallback) {
×
1949
                    if ($instance->request instanceof self) {
×
1950
                        $response = $instance->request->_buildResponse($instance->rawResponse, $instance);
×
1951
                    } else {
1952
                        $response = $instance->rawResponse;
×
1953
                    }
1954

1955
                    $onBeforeSendCallback(
×
1956
                        $response,
×
1957
                        $instance->request,
×
1958
                        $instance
×
1959
                    );
×
1960
                }
×
1961
            );
×
1962
        }
1963

1964
        if ($onErrorCallback !== null) {
27✔
1965
            $this->curlMulti->error(
×
1966
                static function (Curl $instance) use ($onErrorCallback) {
×
1967
                    if ($instance->request instanceof self) {
×
1968
                        $response = $instance->request->_buildResponse($instance->rawResponse, $instance);
×
1969
                    } else {
1970
                        $response = $instance->rawResponse;
×
1971
                    }
1972

1973
                    $onErrorCallback(
×
1974
                        $response,
×
1975
                        $instance->request,
×
1976
                        $instance
×
1977
                    );
×
1978
                }
×
1979
            );
×
1980
        }
1981

1982
        return $this->curlMulti;
27✔
1983
    }
1984

1985
    /**
1986
     * Actually send off the request, and parse the response.
1987
     *
1988
     * @throws NetworkErrorException when unable to parse or communicate w server
1989
     *
1990
     * @return Response
1991
     */
1992
    public function send(): Response
1993
    {
1994
        $this->_curlPrep();
31✔
1995
        $curl = $this->curl;
31✔
1996
        if ($curl === null) {
31✔
NEW
1997
            throw new NetworkErrorException('Unable to initialize cURL.');
×
1998
        }
1999

2000
        $result = $curl->exec();
31✔
2001

2002
        if (
2003
            $result === false
31✔
2004
            &&
2005
            $this->retry_by_possible_encoding_error
31✔
2006
        ) {
2007
            // Possibly a gzip issue makes curl unhappy.
2008
            if (
NEW
2009
                \in_array($this->curl->errorCode, [\CURLE_WRITE_ERROR, \CURLE_BAD_CONTENT_ENCODING], true)
×
2010
            ) {
2011
                // Docs say 'identity,' but 'none' seems to work (sometimes?).
2012
                $this->curl->setOpt(\CURLOPT_ENCODING, 'none');
×
2013

2014
                $result = $this->curl->exec();
×
2015

2016
                if ($result === false) {
×
2017
                    if (
NEW
2018
                        \in_array($this->curl->errorCode, [\CURLE_WRITE_ERROR, \CURLE_BAD_CONTENT_ENCODING], true)
×
2019
                    ) {
2020
                        $this->curl->setOpt(\CURLOPT_ENCODING, 'identity');
×
2021

2022
                        $result = $this->curl->exec();
×
2023
                    }
2024
                }
2025
            }
2026
        }
2027

2028
        if (!$this->keep_alive) {
31✔
2029
            $this->close();
×
2030
        }
2031

2032
        return $this->_buildResponse($result);
31✔
2033
    }
2034

2035
    /**
2036
     * @return static
2037
     */
2038
    public function sendsCsv(): self
2039
    {
2040
        return $this->withContentType(Mime::CSV);
1✔
2041
    }
2042

2043
    /**
2044
     * @return static
2045
     */
2046
    public function sendsForm(): self
2047
    {
2048
        return $this->withContentType(Mime::FORM);
1✔
2049
    }
2050

2051
    /**
2052
     * @return static
2053
     */
2054
    public function sendsHtml(): self
2055
    {
2056
        return $this->withContentType(Mime::HTML);
1✔
2057
    }
2058

2059
    /**
2060
     * @return static
2061
     */
2062
    public function sendsJavascript(): self
2063
    {
2064
        return $this->withContentType(Mime::JS);
1✔
2065
    }
2066

2067
    /**
2068
     * @return static
2069
     */
2070
    public function sendsJs(): self
2071
    {
2072
        return $this->withContentType(Mime::JS);
1✔
2073
    }
2074

2075
    /**
2076
     * @return static
2077
     */
2078
    public function sendsJson(): self
2079
    {
2080
        return $this->withContentType(Mime::JSON);
2✔
2081
    }
2082

2083
    /**
2084
     * @return static
2085
     */
2086
    public function sendsPlain(): self
2087
    {
2088
        return $this->withContentType(Mime::PLAIN);
1✔
2089
    }
2090

2091
    /**
2092
     * @return static
2093
     */
2094
    public function sendsText(): self
2095
    {
2096
        return $this->withContentType(Mime::PLAIN);
1✔
2097
    }
2098

2099
    /**
2100
     * @return static
2101
     */
2102
    public function sendsUpload(): self
2103
    {
2104
        return $this->withContentType(Mime::UPLOAD);
2✔
2105
    }
2106

2107
    /**
2108
     * @return static
2109
     */
2110
    public function sendsXhtml(): self
2111
    {
2112
        return $this->withContentType(Mime::XHTML);
1✔
2113
    }
2114

2115
    /**
2116
     * @return static
2117
     */
2118
    public function sendsXml(): self
2119
    {
2120
        return $this->withContentType(Mime::XML);
1✔
2121
    }
2122

2123
    /**
2124
     * Determine how/if we use the built in serialization by
2125
     * setting the serialize_payload_method
2126
     * The default (SERIALIZE_PAYLOAD_SMART) is...
2127
     *  - if payload is not a scalar (object/array)
2128
     *    use the appropriate serialize method according to
2129
     *    the Content-Type of this request.
2130
     *  - if the payload IS a scalar (int, float, string, bool)
2131
     *    than just return it as is.
2132
     * When this option is set SERIALIZE_PAYLOAD_ALWAYS,
2133
     * it will always use the appropriate
2134
     * serialize option regardless of whether payload is scalar or not
2135
     * When this option is set SERIALIZE_PAYLOAD_NEVER,
2136
     * it will never use any of the serialization methods.
2137
     * Really the only use for this is if you want the serialize methods
2138
     * to handle strings or not (e.g. Blah is not valid JSON, but "Blah"
2139
     * is).  Forcing the serialization helps prevent that kind of error from
2140
     * happening.
2141
     *
2142
     * @param int $mode Request::SERIALIZE_PAYLOAD_*
2143
     *
2144
     * @return static
2145
     */
2146
    public function serializePayloadMode(int $mode): self
2147
    {
2148
        $this->serialize_payload_method = $mode;
9✔
2149

2150
        return $this;
9✔
2151
    }
2152

2153
    /**
2154
     * This method is the default behavior
2155
     *
2156
     * @return static
2157
     *
2158
     * @see Request::serializePayloadMode()
2159
     */
2160
    public function smartSerializePayload(): self
2161
    {
2162
        return $this->serializePayloadMode(static::SERIALIZE_PAYLOAD_SMART);
1✔
2163
    }
2164

2165
    /**
2166
     * Specify a HTTP timeout
2167
     *
2168
     * @param float|int $timeout seconds to timeout the HTTP call
2169
     *
2170
     * @return static
2171
     */
2172
    public function withTimeout($timeout): self
2173
    {
2174
        if (!\preg_match('/^\d+(\.\d+)?/', (string) $timeout)) {
9✔
2175
            throw new \InvalidArgumentException(
1✔
2176
                'Invalid timeout provided: ' . \var_export($timeout, true)
1✔
2177
            );
1✔
2178
        }
2179

2180
        $new = clone $this;
8✔
2181

2182
        $new->timeout = $timeout;
8✔
2183

2184
        return $new;
8✔
2185
    }
2186

2187
    /**
2188
     * @param int|string $maximum_number_of_retries
2189
     *
2190
     * @return static
2191
     */
2192
    public function withRetry($maximum_number_of_retries): self
2193
    {
2194
        if (!\preg_match('/^\d+$/', (string) $maximum_number_of_retries)) {
10✔
2195
            throw new \InvalidArgumentException(
1✔
2196
                'Invalid retry count provided: ' . \var_export($maximum_number_of_retries, true)
1✔
2197
            );
1✔
2198
        }
2199

2200
        $new = clone $this;
9✔
2201
        $new->retry = (int) $maximum_number_of_retries;
9✔
2202

2203
        return $new;
9✔
2204
    }
2205

2206
    /**
2207
     * @param float|int $delay seconds between retry attempts
2208
     *
2209
     * @return static
2210
     */
2211
    public function withRetryDelay($delay): self
2212
    {
2213
        $new = clone $this;
9✔
2214
        $new->retry_delay = $this->_normalizeDurationValue($delay, 'retry delay');
9✔
2215

2216
        return $new;
8✔
2217
    }
2218

2219
    /**
2220
     * @param float|int $max_time overall retry budget in seconds
2221
     *
2222
     * @return static
2223
     */
2224
    public function withRetryMaxTime($max_time): self
2225
    {
2226
        $new = clone $this;
4✔
2227
        $new->retry_max_time = $this->_normalizeDurationValue($max_time, 'retry max time');
4✔
2228

2229
        return $new;
3✔
2230
    }
2231

2232
    /**
2233
     * @return static
2234
     */
2235
    public function withRetryAllErrors(bool $retry_all_errors = true): self
2236
    {
2237
        $new = clone $this;
2✔
2238
        $new->retry_all_errors = $retry_all_errors;
2✔
2239

2240
        return $new;
2✔
2241
    }
2242

2243
    /**
2244
     * @return static
2245
     */
2246
    public function withRetryConnectionRefused(bool $retry_connection_refused = true): self
2247
    {
2248
        $new = clone $this;
2✔
2249
        $new->retry_connection_refused = $retry_connection_refused;
2✔
2250

2251
        return $new;
2✔
2252
    }
2253

2254
    /**
2255
     * Shortcut for useProxy to configure SOCKS 4 proxy
2256
     *
2257
     * @param string   $proxy_host    Hostname or address of the proxy
2258
     * @param int      $proxy_port    Port of the proxy. Default 80
2259
     * @param int|null $auth_type     Authentication type or null. Accepted values are CURLAUTH_BASIC, CURLAUTH_NTLM.
2260
     *                                Default null, no authentication
2261
     * @param string   $auth_username Authentication username. Default null
2262
     * @param string   $auth_password Authentication password. Default null
2263
     *
2264
     * @return static
2265
     *
2266
     * @see Request::withProxy
2267
     */
2268
    public function useSocks4Proxy(
2269
        $proxy_host,
2270
        $proxy_port = 80,
2271
        $auth_type = null,
2272
        $auth_username = null,
2273
        $auth_password = null
2274
    ): self {
2275
        return $this->withProxy(
1✔
2276
            $proxy_host,
1✔
2277
            $proxy_port,
1✔
2278
            $auth_type,
1✔
2279
            $auth_username,
1✔
2280
            $auth_password,
1✔
2281
            Proxy::SOCKS4
1✔
2282
        );
1✔
2283
    }
2284

2285
    /**
2286
     * Shortcut for useProxy to configure SOCKS 5 proxy
2287
     *
2288
     * @param string      $proxy_host
2289
     * @param int         $proxy_port
2290
     * @param int|null    $auth_type
2291
     * @param string|null $auth_username
2292
     * @param string|null $auth_password
2293
     *
2294
     * @return static
2295
     *
2296
     * @see Request::withProxy
2297
     */
2298
    public function useSocks5Proxy(
2299
        $proxy_host,
2300
        $proxy_port = 80,
2301
        $auth_type = null,
2302
        $auth_username = null,
2303
        $auth_password = null
2304
    ): self {
2305
        return $this->withProxy(
1✔
2306
            $proxy_host,
1✔
2307
            $proxy_port,
1✔
2308
            $auth_type,
1✔
2309
            $auth_username,
1✔
2310
            $auth_password,
1✔
2311
            Proxy::SOCKS5
1✔
2312
        );
1✔
2313
    }
2314

2315
    /**
2316
     * @param string $name
2317
     * @param string $value
2318
     *
2319
     * @return static
2320
     */
2321
    public function withAddedCookie(string $name, string $value): self
2322
    {
2323
        return $this->withAddedHeader('Cookie', "{$name}={$value}");
3✔
2324
    }
2325

2326
    /**
2327
     * @param array<string,string> $files
2328
     *
2329
     * @return static
2330
     */
2331
    public function withAttachment($files): self
2332
    {
2333
        $new = clone $this;
1✔
2334

2335
        $fInfo = \finfo_open(\FILEINFO_MIME_TYPE);
1✔
2336
        if ($fInfo === false) {
1✔
2337
            /** @noinspection ForgottenDebugOutputInspection */
NEW
2338
            \error_log('finfo_open() did not work');
×
2339

2340
            return $new;
×
2341
        }
2342

2343
        foreach ($files as $key => $file) {
1✔
2344
            $mimeType = \finfo_file($fInfo, $file);
1✔
2345
            if ($mimeType !== false) {
1✔
2346
                if (\is_string($new->payload)) {
1✔
2347
                    $new->payload = []; // reset
×
2348
                }
2349
                $new->payload[$key] = \curl_file_create($file, $mimeType, \basename($file));
1✔
2350
            }
2351
        }
2352

2353
        \finfo_close($fInfo);
1✔
2354

2355
        return $new->_withContentType(Mime::UPLOAD);
1✔
2356
    }
2357

2358
    /**
2359
     * User Basic Auth.
2360
     *
2361
     * Only use when over SSL/TSL/HTTPS.
2362
     *
2363
     * @param string $username
2364
     * @param string $password
2365
     *
2366
     * @return static
2367
     */
2368
    public function withBasicAuth($username, $password): self
2369
    {
2370
        $new = clone $this;
13✔
2371
        $new->username = $username;
13✔
2372
        $new->password = $password;
13✔
2373

2374
        return $new;
13✔
2375
    }
2376

2377
    /**
2378
     * @param string $token
2379
     *
2380
     * @return static
2381
     */
2382
    public function withBearerToken(string $token): self
2383
    {
2384
        return $this->withHeader('Authorization', 'Bearer ' . $token);
1✔
2385
    }
2386

2387
    /**
2388
     * @param array<string|int, mixed> $body
2389
     *
2390
     * @return static
2391
     */
2392
    public function withBodyFromArray(array $body)
2393
    {
2394
        return $this->_setBody($body, null);
3✔
2395
    }
2396

2397
    /**
2398
     * @param string $body
2399
     *
2400
     * @return static
2401
     */
2402
    public function withBodyFromString(string $body)
2403
    {
2404
        $stream = Http::stream($body);
10✔
2405

2406
        return $this->_setBody($stream->getContents(), null);
10✔
2407
    }
2408

2409
    /**
2410
     * Specify a HTTP connection timeout
2411
     *
2412
     * @param float|int $connection_timeout seconds to timeout the HTTP connection
2413
     *
2414
     * @throws \InvalidArgumentException
2415
     *
2416
     * @return static
2417
     */
2418
    public function withConnectionTimeoutInSeconds($connection_timeout): self
2419
    {
2420
        if (!\preg_match('/^\d+(\.\d+)?/', (string) $connection_timeout)) {
7✔
2421
            throw new \InvalidArgumentException(
1✔
2422
                'Invalid connection timeout provided: ' . \var_export($connection_timeout, true)
1✔
2423
            );
1✔
2424
        }
2425

2426
        $new = clone $this;
6✔
2427

2428
        $new->connection_timeout = $connection_timeout;
6✔
2429

2430
        return $new;
6✔
2431
    }
2432

2433
    /**
2434
     * @param string $ca_bundle_path
2435
     *
2436
     * @return static
2437
     */
2438
    public function withCaBundle(string $ca_bundle_path): self
2439
    {
2440
        return $this->_withNamedCurlOption('CURLOPT_CAINFO', $ca_bundle_path);
1✔
2441
    }
2442

2443
    /**
2444
     * @param string $ca_path
2445
     *
2446
     * @return static
2447
     */
2448
    public function withCaPath(string $ca_path): self
2449
    {
2450
        return $this->_withNamedCurlOption('CURLOPT_CAPATH', $ca_path);
1✔
2451
    }
2452

2453
    /**
2454
     * @param string     $minimum_version
2455
     * @param string|int $maximum_version
2456
     *
2457
     * @return static
2458
     */
2459
    public function withTlsVersion($minimum_version, $maximum_version = null): self
2460
    {
2461
        [$minimum_option, $minimum_rank] = $this->_normalizeTlsVersionOption($minimum_version, false);
4✔
2462
        $ssl_version = $minimum_option;
3✔
2463

2464
        if ($maximum_version !== null) {
3✔
2465
            [$maximum_option, $maximum_rank] = $this->_normalizeTlsVersionOption($maximum_version, true);
2✔
2466

2467
            if (
2468
                $minimum_rank !== null
2✔
2469
                &&
2470
                $maximum_rank !== null
2✔
2471
                &&
2472
                $minimum_rank > $maximum_rank
2✔
2473
            ) {
2474
                throw new \InvalidArgumentException('The minimum TLS version cannot be greater than the maximum TLS version.');
1✔
2475
            }
2476

2477
            $ssl_version |= $maximum_option;
1✔
2478
        }
2479

2480
        return $this->_withNamedCurlOption('CURLOPT_SSLVERSION', $ssl_version);
2✔
2481
    }
2482

2483
    /**
2484
     * @param string $pinned_public_key
2485
     *
2486
     * @return static
2487
     */
2488
    public function withPinnedPublicKey(string $pinned_public_key): self
2489
    {
2490
        return $this->_withNamedCurlOption('CURLOPT_PINNEDPUBLICKEY', $pinned_public_key);
1✔
2491
    }
2492

2493
    /**
2494
     * @param string $cache_control
2495
     *                              <p>e.g. 'no-cache', 'public', ...</p>
2496
     *
2497
     * @return static
2498
     */
2499
    public function withCacheControl(string $cache_control): self
2500
    {
2501
        $new = clone $this;
6✔
2502

2503
        if (empty($cache_control)) {
6✔
2504
            return $new;
1✔
2505
        }
2506

2507
        $new->cache_control = $cache_control;
5✔
2508

2509
        return $new;
5✔
2510
    }
2511

2512
    /**
2513
     * @param string $charset
2514
     *                        <p>e.g. "UTF-8"</p>
2515
     *
2516
     * @return static
2517
     */
2518
    public function withContentCharset(string $charset): self
2519
    {
2520
        $new = clone $this;
2✔
2521

2522
        if (empty($charset)) {
2✔
2523
            return $new;
1✔
2524
        }
2525

2526
        $new->content_charset = UTF8::normalize_encoding($charset);
1✔
2527

2528
        return $new;
1✔
2529
    }
2530

2531
    /**
2532
     * @param int $port
2533
     *
2534
     * @return static
2535
     */
2536
    public function withPort(int $port): self
2537
    {
2538
        $new = clone $this;
5✔
2539

2540
        $new->port = $port;
5✔
2541
        if ($new->uri) {
5✔
2542
            $new->uri = $new->uri->withPort($port);
4✔
2543
            $new->_updateHostFromUri();
4✔
2544
        }
2545

2546
        return $new;
5✔
2547
    }
2548

2549
    /**
2550
     * @param string $encoding
2551
     *
2552
     * @return static
2553
     */
2554
    public function withContentEncoding(string $encoding): self
2555
    {
2556
        $new = clone $this;
6✔
2557

2558
        $new->content_encoding = $encoding;
6✔
2559

2560
        return $new;
6✔
2561
    }
2562

2563
    /**
2564
     * @param string|null $mime     use a constant from Mime::*
2565
     * @param string|null $fallback use a constant from Mime::*
2566
     *
2567
     * @return static
2568
     */
2569
    public function withContentType($mime, ?string $fallback = null): self
2570
    {
2571
        return (clone $this)->_withContentType($mime, $fallback);
7✔
2572
    }
2573

2574
    /**
2575
     * @return static
2576
     */
2577
    public function withContentTypeCsv(): self
2578
    {
2579
        $new = clone $this;
1✔
2580
        $new->content_type = Mime::getFullMime(Mime::CSV);
1✔
2581

2582
        return $new;
1✔
2583
    }
2584

2585
    /**
2586
     * @return static
2587
     */
2588
    public function withContentTypeForm(): self
2589
    {
2590
        $new = clone $this;
2✔
2591
        $new->content_type = Mime::getFullMime(Mime::FORM);
2✔
2592

2593
        return $new;
2✔
2594
    }
2595

2596
    /**
2597
     * @return static
2598
     */
2599
    public function withContentTypeHtml(): self
2600
    {
2601
        $new = clone $this;
1✔
2602
        $new->content_type = Mime::getFullMime(Mime::HTML);
1✔
2603

2604
        return $new;
1✔
2605
    }
2606

2607
    /**
2608
     * @return static
2609
     */
2610
    public function withContentTypeJson(): self
2611
    {
2612
        $new = clone $this;
1✔
2613
        $new->content_type = Mime::getFullMime(Mime::JSON);
1✔
2614

2615
        return $new;
1✔
2616
    }
2617

2618
    /**
2619
     * @return static
2620
     */
2621
    public function withContentTypePlain(): self
2622
    {
2623
        $new = clone $this;
1✔
2624
        $new->content_type = Mime::getFullMime(Mime::PLAIN);
1✔
2625

2626
        return $new;
1✔
2627
    }
2628

2629
    /**
2630
     * @return static
2631
     */
2632
    public function withContentTypeXml(): self
2633
    {
2634
        $new = clone $this;
1✔
2635
        $new->content_type = Mime::getFullMime(Mime::XML);
1✔
2636

2637
        return $new;
1✔
2638
    }
2639

2640
    /**
2641
     * @return static
2642
     */
2643
    public function withContentTypeYaml(): self
2644
    {
2645
        return $this->withContentType(Mime::YAML);
1✔
2646
    }
2647

2648
    /**
2649
     * @param string $name
2650
     * @param string $value
2651
     *
2652
     * @return static
2653
     */
2654
    public function withCookie(string $name, string $value): self
2655
    {
2656
        return $this->withHeader('Cookie', "{$name}={$value}");
2✔
2657
    }
2658

2659
    /**
2660
     * @param string $cookie_file
2661
     *
2662
     * @return static
2663
     */
2664
    public function withCookieFile(string $cookie_file): self
2665
    {
2666
        return $this->_withNamedCurlOption('CURLOPT_COOKIEFILE', $cookie_file);
1✔
2667
    }
2668

2669
    /**
2670
     * @param string $cookie_jar
2671
     *
2672
     * @return static
2673
     */
2674
    public function withCookieJar(string $cookie_jar): self
2675
    {
2676
        return $this->_withNamedCurlOption('CURLOPT_COOKIEJAR', $cookie_jar);
1✔
2677
    }
2678

2679
    /**
2680
     * Semi-reluctantly added this as a way to add in curl opts
2681
     * that are not otherwise accessible from the rest of the API.
2682
     *
2683
     * @param int   $curl_opt
2684
     * @param mixed $curl_opt_val
2685
     *
2686
     * @return static
2687
     */
2688
    public function withCurlOption($curl_opt, $curl_opt_val): self
2689
    {
2690
        $new = clone $this;
15✔
2691

2692
        $new->_withCurlOptionValue($curl_opt, $curl_opt_val);
15✔
2693

2694
        return $new;
15✔
2695
    }
2696

2697
    /**
2698
     * @param string $filename
2699
     *
2700
     * @return static
2701
     */
2702
    public function withAltSvcCache(string $filename = '', bool $read_only = false): self
2703
    {
2704
        $new = $this->_withNamedCurlOption('CURLOPT_ALTSVC', $filename);
1✔
2705

2706
        return $new->_withNamedCurlOption(
1✔
2707
            'CURLOPT_ALTSVC_CTRL',
1✔
2708
            $this->_buildAltSvcControlValue($read_only)
1✔
2709
        );
1✔
2710
    }
2711

2712
    /**
2713
     * @param string|null $filename
2714
     *
2715
     * @return static
2716
     */
2717
    public function withHstsCache($filename = null, bool $read_only = false): self
2718
    {
2719
        $new = $this->_withNamedCurlOption('CURLOPT_HSTS', $filename);
1✔
2720

2721
        return $new->_withNamedCurlOption(
1✔
2722
            'CURLOPT_HSTS_CTRL',
1✔
2723
            $this->_buildHstsControlValue($read_only)
1✔
2724
        );
1✔
2725
    }
2726

2727
    /**
2728
     * User Digest Auth.
2729
     *
2730
     * @param string $username
2731
     * @param string $password
2732
     *
2733
     * @return static
2734
     */
2735
    public function withDigestAuth($username, $password): self
2736
    {
2737
        $new = clone $this;
4✔
2738

2739
        $new = $new->withCurlOption(\CURLOPT_HTTPAUTH, \CURLAUTH_DIGEST);
4✔
2740

2741
        return $new->withBasicAuth($username, $password);
4✔
2742
    }
2743

2744
    /**
2745
     * Callback called to handle HTTP errors. When nothing is set, defaults
2746
     * to logging via `error_log`.
2747
     *
2748
     * @param callable|LoggerInterface|null $error_handler
2749
     *
2750
     * @return static
2751
     */
2752
    public function withErrorHandler($error_handler): self
2753
    {
2754
        $new = clone $this;
4✔
2755

2756
        $new->error_handler = $error_handler;
4✔
2757

2758
        return $new;
4✔
2759
    }
2760

2761
    /**
2762
     * @param string|null $mime     use a constant from Mime::*
2763
     * @param string|null $fallback use a constant from Mime::*
2764
     *
2765
     * @return static
2766
     */
2767
    public function withExpectedType($mime, ?string $fallback = null): self
2768
    {
2769
        return (clone $this)->_withExpectedType($mime, $fallback);
9✔
2770
    }
2771

2772
    /**
2773
     * @param string[]|string[][] $header
2774
     *
2775
     * @return static
2776
     */
2777
    public function withHeaders(array $header): self
2778
    {
2779
        $new = clone $this;
4✔
2780

2781
        foreach ($header as $name => $value) {
4✔
2782
            $new = $new->withAddedHeader($name, $value);
4✔
2783
        }
2784

2785
        return $new;
4✔
2786
    }
2787

2788
    /**
2789
     * Helper function to set the Content type and Expected as same in one swoop.
2790
     *
2791
     * @param string|null $mime
2792
     *                          <p>\Httpful\Mime::JSON, \Httpful\Mime::XML, ...</p>
2793
     *
2794
     * @return static
2795
     */
2796
    public function withMimeType($mime): self
2797
    {
2798
        return (clone $this)->_withMimeType($mime);
262✔
2799
    }
2800

2801
    /**
2802
     * @param string $username
2803
     * @param string $password
2804
     *
2805
     * @return static
2806
     */
2807
    public function withNtlmAuth($username, $password): self
2808
    {
2809
        $new = clone $this;
2✔
2810

2811
        $new = $new->withCurlOption(\CURLOPT_HTTPAUTH, \CURLAUTH_NTLM);
2✔
2812

2813
        return $new->withBasicAuth($username, $password);
2✔
2814
    }
2815

2816
    /**
2817
     * Add additional parameter to be appended to the query string.
2818
     *
2819
     * @param int|string|null $key
2820
     * @param int|string|null $value
2821
     *
2822
     * @return static
2823
     */
2824
    public function withParam($key, $value): self
2825
    {
2826
        $new = clone $this;
5✔
2827

2828
        if (
2829
            isset($key, $value)
5✔
2830
            &&
2831
            $key !== ''
5✔
2832
        ) {
2833
            $new->params[$key] = $value;
4✔
2834
        }
2835

2836
        return $new;
5✔
2837
    }
2838

2839
    /**
2840
     * Add additional parameters to be appended to the query string.
2841
     *
2842
     * Takes an associative array of key/value pairs as an argument.
2843
     *
2844
     * @param array<string|int, mixed> $params
2845
     *
2846
     * @return static this
2847
     */
2848
    public function withParams(array $params): self
2849
    {
2850
        $new = clone $this;
3✔
2851

2852
        $new->params = \array_merge($new->params, $params);
3✔
2853

2854
        return $new;
3✔
2855
    }
2856

2857
    /**
2858
     * Use a custom function to parse the response.
2859
     *
2860
     * @param callable $callback Takes the raw body of
2861
     *                           the http response and returns a mixed
2862
     *
2863
     * @return static
2864
     */
2865
    public function withParseCallback(callable $callback): self
2866
    {
2867
        $new = clone $this;
3✔
2868

2869
        $new->parse_callback = $callback;
3✔
2870

2871
        return $new;
3✔
2872
    }
2873

2874
    /**
2875
     * Use proxy configuration
2876
     *
2877
     * @param string   $proxy_host    Hostname or address of the proxy
2878
     * @param int      $proxy_port    Port of the proxy. Default 80
2879
     * @param int|null $auth_type     Authentication type or null. Accepted values are CURLAUTH_BASIC, CURLAUTH_NTLM.
2880
     *                                Default null, no authentication
2881
     * @param string   $auth_username Authentication username. Default null
2882
     * @param string   $auth_password Authentication password. Default null
2883
     * @param int      $proxy_type    Proxy-Type for Curl. Default is "Proxy::HTTP"
2884
     *
2885
     * @return static
2886
     */
2887
    public function withProxy(
2888
        $proxy_host,
2889
        $proxy_port = 80,
2890
        $auth_type = null,
2891
        $auth_username = null,
2892
        $auth_password = null,
2893
        $proxy_type = Proxy::HTTP
2894
    ): self {
2895
        $new = clone $this;
7✔
2896

2897
        $new = $new->withCurlOption(\CURLOPT_PROXY, "{$proxy_host}:{$proxy_port}");
7✔
2898
        $new = $new->withCurlOption(\CURLOPT_PROXYTYPE, $proxy_type);
7✔
2899

2900
        if (\in_array($auth_type, [\CURLAUTH_BASIC, \CURLAUTH_NTLM], true)) {
7✔
2901
            $new = $new->withCurlOption(\CURLOPT_PROXYAUTH, $auth_type);
1✔
2902
            $new = $new->withCurlOption(\CURLOPT_PROXYUSERPWD, "{$auth_username}:{$auth_password}");
1✔
2903
        }
2904

2905
        return $new;
7✔
2906
    }
2907

2908
    /**
2909
     * @return static
2910
     */
2911
    public function withProxyTunnel(bool $tunnel = true): self
2912
    {
2913
        return $this->_withNamedCurlOption('CURLOPT_HTTPPROXYTUNNEL', $tunnel);
1✔
2914
    }
2915

2916
    /**
2917
     * @param string[]|string $hosts
2918
     *
2919
     * @return static
2920
     */
2921
    public function withNoProxy($hosts): self
2922
    {
2923
        if (\is_array($hosts)) {
1✔
2924
            $hosts = \implode(',', $hosts);
1✔
2925
        }
2926

2927
        return $this->_withNamedCurlOption('CURLOPT_NOPROXY', (string) $hosts);
1✔
2928
    }
2929

2930
    /**
2931
     * @param string[]|string $entries
2932
     *
2933
     * @return static
2934
     */
2935
    public function withResolve($entries): self
2936
    {
2937
        return $this->_withNamedCurlOption(
3✔
2938
            'CURLOPT_RESOLVE',
3✔
2939
            $this->_normalizeCurlStringList($entries, 'resolve entries')
3✔
2940
        );
3✔
2941
    }
2942

2943
    /**
2944
     * @param string[]|string $entries
2945
     *
2946
     * @return static
2947
     */
2948
    public function withConnectTo($entries): self
2949
    {
2950
        return $this->_withNamedCurlOption(
2✔
2951
            'CURLOPT_CONNECT_TO',
2✔
2952
            $this->_normalizeCurlStringList($entries, 'connect-to entries')
2✔
2953
        );
2✔
2954
    }
2955

2956
    /**
2957
     * @param string|null $key
2958
     * @param mixed|null  $fallback
2959
     *
2960
     * @return mixed
2961
     */
2962
    public function getHelperData($key = null, $fallback = null)
2963
    {
2964
        if ($key !== null) {
2✔
2965
            return $this->helperData[$key] ?? $fallback;
2✔
2966
        }
2967

2968
        return $this->helperData;
1✔
2969
    }
2970

2971
    /**
2972
     * @return void
2973
     */
2974
    public function clearHelperData()
2975
    {
2976
        $this->helperData = [];
8✔
2977
    }
2978

2979
    /**
2980
     * @param string $key
2981
     * @param mixed  $value
2982
     *
2983
     * @return static
2984
     */
2985
    public function addHelperData(string $key, $value): self
2986
    {
2987
        $this->helperData[$key] = $value;
2✔
2988

2989
        return $this;
2✔
2990
    }
2991

2992
    /**
2993
     * @param callable|null $send_callback
2994
     *
2995
     * @return static
2996
     */
2997
    public function withSendCallback($send_callback): self
2998
    {
2999
        $new = clone $this;
2✔
3000

3001
        if (!empty($send_callback)) {
2✔
3002
            $new->send_callbacks[] = $send_callback;
1✔
3003
        }
3004

3005
        return $new;
2✔
3006
    }
3007

3008
    /**
3009
     * @param callable $callback
3010
     *
3011
     * @return static
3012
     */
3013
    public function withSerializePayload(callable $callback): self
3014
    {
3015
        return $this->registerPayloadSerializer('*', $callback);
1✔
3016
    }
3017

3018
    /**
3019
     * @param string $file_path
3020
     *
3021
     * @return Request
3022
     */
3023
    public function withDownload($file_path): self
3024
    {
3025
        $new = clone $this;
4✔
3026

3027
        $new->file_path_for_download = $file_path;
4✔
3028

3029
        return $new;
4✔
3030
    }
3031

3032
    /**
3033
     * @param string $uri
3034
     * @param bool   $useClone
3035
     *
3036
     * @return static
3037
     */
3038
    public function withUriFromString(string $uri, bool $useClone = true): self
3039
    {
3040
        if ($useClone) {
334✔
3041
            return (clone $this)->withUri(new Uri($uri));
334✔
3042
        }
3043

3044
        return $this->_withUri(new Uri($uri));
2✔
3045
    }
3046

3047
    /**
3048
     * Sets user agent.
3049
     *
3050
     * @param string $userAgent
3051
     *
3052
     * @return static
3053
     */
3054
    public function withUserAgent($userAgent): self
3055
    {
3056
        return $this->withHeader('User-Agent', $userAgent);
2✔
3057
    }
3058

3059
    /**
3060
     * Takes a curl result and generates a Response from it.
3061
     *
3062
     * @param false|mixed $result
3063
     * @param Curl|null   $curl
3064
     *
3065
     * @throws NetworkErrorException
3066
     *
3067
     * @return Response
3068
     *
3069
     * @internal
3070
     */
3071
    public function _buildResponse($result, ?Curl $curl = null): Response
3072
    {
3073
        // fallback
3074
        if ($curl === null) {
37✔
3075
            $curl = $this->curl;
31✔
3076
        }
3077

3078
        if ($curl === null) {
37✔
3079
            throw new NetworkErrorException('Unable to build the response for "' . $this->uri . '". => "curl" === null');
×
3080
        }
3081

3082
        if ($result === false) {
37✔
3083
            $curlErrorNumber = $curl->getErrorCode();
7✔
3084
            if ($curlErrorNumber) {
7✔
3085
                $curlErrorString = (string) $curl->getErrorMessage();
7✔
3086

3087
                $this->_error($curlErrorString);
7✔
3088

3089
                $exception = new NetworkErrorException(
7✔
3090
                    'Unable to connect to "' . $this->uri . '": ' . $curlErrorNumber . ' ' . $curlErrorString,
7✔
3091
                    $curlErrorNumber,
7✔
3092
                    null,
7✔
3093
                    $curl,
7✔
3094
                    $this
7✔
3095
                );
7✔
3096

3097
                $exception->setCurlErrorNumber($curlErrorNumber)->setCurlErrorString($curlErrorString);
7✔
3098

3099
                throw $exception;
7✔
3100
            }
3101

3102
            $this->_error('Unable to connect to "' . $this->uri . '".');
×
3103

3104
            throw new NetworkErrorException('Unable to connect to "' . $this->uri . '".');
×
3105
        }
3106

3107
        $curl_info = $curl->getInfo();
30✔
3108

3109
        $headers = $curl->getRawResponseHeaders();
30✔
3110
        $rawResponse = $curl->getRawResponse();
30✔
3111

3112
        if ($rawResponse === false) {
30✔
3113
            $body = '';
×
3114
        } elseif ($rawResponse === true && $this->file_path_for_download && \is_string($this->file_path_for_download)) {
30✔
3115
            $body = \file_get_contents($this->file_path_for_download);
1✔
3116
            if ($body === false) {
1✔
3117
                throw new \ErrorException('file_get_contents return false for: ' . $this->file_path_for_download);
×
3118
            }
3119
        } else {
3120
            $body = UTF8::remove_left(
29✔
3121
                (string) $rawResponse,
29✔
3122
                $headers
29✔
3123
            );
29✔
3124
        }
3125

3126
        // get the protocol + version
3127
        $protocol_version_regex = "/HTTP\/(?<version>[\d\.]*+)/i";
30✔
3128
        $protocol_version_matches = [];
30✔
3129
        $protocol_version = null;
30✔
3130
        \preg_match($protocol_version_regex, $headers, $protocol_version_matches);
30✔
3131
        if (isset($protocol_version_matches['version'])) {
30✔
3132
            $protocol_version = $protocol_version_matches['version'];
29✔
3133
        }
3134
        $curl_info['protocol_version'] = $protocol_version;
30✔
3135

3136
        // DEBUG
3137
        //var_dump($body, $headers);
3138

3139
        return new Response(
30✔
3140
            $body,
30✔
3141
            $headers,
30✔
3142
            $this,
30✔
3143
            $curl_info
30✔
3144
        );
30✔
3145
    }
3146

3147
    /**
3148
     * @return void
3149
     */
3150
    private function _configureRetryBehavior(): void
3151
    {
3152
        $curl = $this->curl;
101✔
3153
        if ($curl === null) {
101✔
NEW
3154
            throw new \LogicException('cURL is not initialized.');
×
3155
        }
3156

3157
        $curl->attempts = 0;
101✔
3158
        $curl->retries = 0;
101✔
3159

3160
        if ($this->retry <= 0) {
101✔
3161
            $curl->setRetry(0);
93✔
3162

3163
            return;
93✔
3164
        }
3165

3166
        $curl->setRetry($this->_createRetryDecider());
8✔
3167
    }
3168

3169
    /**
3170
     * @return callable
3171
     */
3172
    private function _createRetryDecider(): callable
3173
    {
3174
        $maximum_number_of_retries = $this->retry;
8✔
3175
        $retry_delay = $this->retry_delay;
8✔
3176
        $retry_max_time = $this->retry_max_time;
8✔
3177
        $retry_all_errors = $this->retry_all_errors;
8✔
3178
        $retry_connection_refused = $this->retry_connection_refused;
8✔
3179
        $started_at = null;
8✔
3180
        $retry_attempts = 0;
8✔
3181

3182
        return static function (Curl $curl) use (
8✔
3183
            $maximum_number_of_retries,
8✔
3184
            $retry_delay,
8✔
3185
            $retry_max_time,
8✔
3186
            $retry_all_errors,
8✔
3187
            $retry_connection_refused,
8✔
3188
            &$started_at,
8✔
3189
            &$retry_attempts
8✔
3190
        ): bool {
8✔
3191
            if ($retry_attempts >= $maximum_number_of_retries) {
8✔
3192
                return false;
1✔
3193
            }
3194

3195
            if (
3196
                !$retry_all_errors
8✔
3197
                &&
3198
                !self::_isRetryableError($curl, $retry_connection_refused)
8✔
3199
            ) {
3200
                return false;
1✔
3201
            }
3202

3203
            $delay_in_seconds = $retry_delay;
7✔
3204
            if ($delay_in_seconds === null) {
7✔
3205
                $delay_in_seconds = \min(600.0, (float) (2 ** $retry_attempts));
1✔
3206
            }
3207

3208
            if ($started_at === null) {
7✔
3209
                $started_at = \microtime(true);
7✔
3210
            }
3211

3212
            if (
3213
                $retry_max_time !== null
7✔
3214
                &&
3215
                ((\microtime(true) - $started_at) + $delay_in_seconds) > $retry_max_time
7✔
3216
            ) {
3217
                return false;
1✔
3218
            }
3219

3220
            if ($delay_in_seconds > 0) {
6✔
3221
                \usleep((int) \round($delay_in_seconds * 1000000));
2✔
3222
            }
3223

3224
            ++$retry_attempts;
6✔
3225

3226
            return true;
6✔
3227
        };
8✔
3228
    }
3229

3230
    /**
3231
     * @return bool
3232
     */
3233
    private static function _isRetryableError(Curl $curl, bool $retry_connection_refused): bool
3234
    {
3235
        if ($curl->isCurlError()) {
8✔
3236
            if ($curl->getCurlErrorCode() === \CURLE_OPERATION_TIMEOUTED) {
3✔
3237
                return true;
1✔
3238
            }
3239

3240
            if (
3241
                $retry_connection_refused
2✔
3242
                &&
3243
                $curl->getCurlErrorCode() === \CURLE_COULDNT_CONNECT
2✔
3244
                &&
3245
                \stripos((string) $curl->getCurlErrorMessage(), 'Connection refused') !== false
2✔
3246
            ) {
3247
                return true;
1✔
3248
            }
3249

3250
            return false;
1✔
3251
        }
3252

3253
        return \in_array(
5✔
3254
            $curl->getHttpStatusCode(),
5✔
3255
            [408, 429, 500, 502, 503, 504],
5✔
3256
            true
5✔
3257
        );
5✔
3258
    }
3259

3260
    /**
3261
     * @return int
3262
     */
3263
    private static function _requireCurlConstant(string $constant_name): int
3264
    {
3265
        if (!\defined($constant_name)) {
5✔
NEW
3266
            throw new \RuntimeException('The installed cURL extension does not support ' . $constant_name . '.');
×
3267
        }
3268

3269
        return (int) \constant($constant_name);
5✔
3270
    }
3271

3272
    /**
3273
     * @return static
3274
     */
3275
    private function _withCurlHttpVersion(string $protocol_version, string $curl_http_version_constant_name): self
3276
    {
3277
        $new = clone $this;
1✔
3278
        $new->protocol_version = $protocol_version;
1✔
3279
        $new->curl_http_version = $curl_http_version_constant_name;
1✔
3280

3281
        return $new;
1✔
3282
    }
3283

3284
    /**
3285
     * @return int
3286
     */
3287
    private function _resolveCurlHttpVersion(): int
3288
    {
3289
        if (\is_int($this->curl_http_version)) {
101✔
3290
            return $this->curl_http_version;
2✔
3291
        }
3292

3293
        if (\is_string($this->curl_http_version) && $this->curl_http_version !== '') {
99✔
NEW
3294
            return self::_requireCurlConstant($this->curl_http_version);
×
3295
        }
3296

3297
        switch ((string) $this->protocol_version) {
99✔
3298
            case Http::HTTP_1_0:
3299
                return \CURL_HTTP_VERSION_1_0;
1✔
3300
            case Http::HTTP_1_1:
99✔
3301
                return \CURL_HTTP_VERSION_1_1;
97✔
3302
            case Http::HTTP_2_0:
2✔
3303
                return \CURL_HTTP_VERSION_2_0;
1✔
3304
            case Http::HTTP_3:
2✔
NEW
3305
                return self::_requireCurlConstant('CURL_HTTP_VERSION_3');
×
3306
            default:
3307
                return \CURL_HTTP_VERSION_NONE;
2✔
3308
        }
3309
    }
3310

3311
    /**
3312
     * @param mixed $value
3313
     *
3314
     * @return static
3315
     */
3316
    private function _withNamedCurlOption(string $curl_option_constant_name, $value): self
3317
    {
3318
        $new = clone $this;
4✔
3319
        $new->_withCurlOptionValue(self::_requireCurlConstant($curl_option_constant_name), $value);
4✔
3320

3321
        return $new;
4✔
3322
    }
3323

3324
    /**
3325
     * @param mixed $curl_opt_val
3326
     *
3327
     * @return static
3328
     */
3329
    private function _withCurlOptionValue(int $curl_opt, $curl_opt_val): self
3330
    {
3331
        $this->additional_curl_opts[$curl_opt] = $curl_opt_val;
19✔
3332

3333
        return $this;
19✔
3334
    }
3335

3336
    /**
3337
     * @param array<int, mixed>|string $entries
3338
     *
3339
     * @return string[]
3340
     */
3341
    private function _normalizeCurlStringList($entries, string $description): array
3342
    {
3343
        if (!\is_array($entries)) {
3✔
3344
            $entries = [$entries];
1✔
3345
        }
3346

3347
        $normalized_entries = [];
3✔
3348
        foreach ($entries as $entry) {
3✔
3349
            if (!\is_string($entry) || $entry === '') {
3✔
3350
                throw new \InvalidArgumentException('Invalid ' . $description . ' provided: ' . \var_export($entry, true));
1✔
3351
            }
3352

3353
            $normalized_entries[] = $entry;
2✔
3354
        }
3355

3356
        return $normalized_entries;
2✔
3357
    }
3358

3359
    /**
3360
     * @param string|int $version
3361
     *
3362
     * @return array{0:int,1:int|null}
3363
     */
3364
    private function _normalizeTlsVersionOption($version, bool $maximum): array
3365
    {
3366
        if (\is_int($version)) {
4✔
3367
            return [$version, null];
1✔
3368
        }
3369

3370
        $normalized_version = \strtolower(\str_replace(['tls', 'v'], '', \trim((string) $version)));
4✔
3371

3372
        if ($normalized_version === 'default') {
4✔
3373
            return [self::_requireCurlConstant('CURL_SSLVERSION_DEFAULT'), 0];
1✔
3374
        }
3375

3376
        $map = [
3✔
3377
            '1'   => ['CURL_SSLVERSION_TLSv1', 1],
3✔
3378
            '1.0' => [$maximum ? 'CURL_SSLVERSION_MAX_TLSv1_0' : 'CURL_SSLVERSION_TLSv1_0', 1],
3✔
3379
            '1.1' => [$maximum ? 'CURL_SSLVERSION_MAX_TLSv1_1' : 'CURL_SSLVERSION_TLSv1_1', 2],
3✔
3380
            '1.2' => [$maximum ? 'CURL_SSLVERSION_MAX_TLSv1_2' : 'CURL_SSLVERSION_TLSv1_2', 3],
3✔
3381
            '1.3' => [$maximum ? 'CURL_SSLVERSION_MAX_TLSv1_3' : 'CURL_SSLVERSION_TLSv1_3', 4],
3✔
3382
        ];
3✔
3383

3384
        if (!isset($map[$normalized_version])) {
3✔
3385
            throw new \InvalidArgumentException('Invalid TLS version provided: ' . \var_export($version, true));
1✔
3386
        }
3387

3388
        return [self::_requireCurlConstant($map[$normalized_version][0]), $map[$normalized_version][1]];
2✔
3389
    }
3390

3391
    /**
3392
     * @return int
3393
     */
3394
    private function _buildAltSvcControlValue(bool $read_only): int
3395
    {
3396
        $control = 0;
1✔
3397

3398
        foreach (['CURLALTSVC_H1', 'CURLALTSVC_H2', 'CURLALTSVC_H3'] as $constant_name) {
1✔
3399
            if (\defined($constant_name)) {
1✔
3400
                $control |= (int) \constant($constant_name);
1✔
3401
            }
3402
        }
3403

3404
        if ($control === 0) {
1✔
NEW
3405
            throw new \RuntimeException('The installed cURL extension does not support Alt-Svc control flags.');
×
3406
        }
3407

3408
        if ($read_only) {
1✔
3409
            $control |= self::_requireCurlConstant('CURLALTSVC_READONLYFILE');
1✔
3410
        }
3411

3412
        return $control;
1✔
3413
    }
3414

3415
    /**
3416
     * @return int
3417
     */
3418
    private function _buildHstsControlValue(bool $read_only): int
3419
    {
3420
        $control = self::_requireCurlConstant('CURLHSTS_ENABLE');
1✔
3421

3422
        if ($read_only) {
1✔
3423
            $control |= self::_requireCurlConstant('CURLHSTS_READONLYFILE');
1✔
3424
        }
3425

3426
        return $control;
1✔
3427
    }
3428

3429
    /**
3430
     * @param float|int $value
3431
     *
3432
     * @return float|int
3433
     */
3434
    private function _normalizeDurationValue($value, string $description)
3435
    {
3436
        if (!\preg_match('/^\d+(\.\d+)?$/', (string) $value)) {
10✔
3437
            throw new \InvalidArgumentException(
2✔
3438
                'Invalid ' . $description . ' provided: ' . \var_export($value, true)
2✔
3439
            );
2✔
3440
        }
3441

3442
        return $value + 0;
8✔
3443
    }
3444

3445
    /**
3446
     * @param bool $auto_parse perform automatic "smart"
3447
     *                         parsing based on Content-Type or "expectedType"
3448
     *                         If not auto parsing, Response->body returns the body
3449
     *                         as a string
3450
     *
3451
     * @return static
3452
     */
3453
    private function _autoParse(bool $auto_parse = true): self
3454
    {
3455
        $new = clone $this;
7✔
3456

3457
        $new->auto_parse = $auto_parse;
7✔
3458

3459
        return $new;
7✔
3460
    }
3461

3462
    /**
3463
     * @param mixed $str payload
3464
     *
3465
     * @return int length of payload in bytes
3466
     */
3467
    private function _determineLength($str): int
3468
    {
3469
        if ($str === null || \is_array($str)) {
18✔
3470
            return 0;
1✔
3471
        }
3472

3473
        return \strlen((string) $str);
17✔
3474
    }
3475

3476
    /**
3477
     * @param string $error
3478
     *
3479
     * @return void
3480
     */
3481
    private function _error($error)
3482
    {
3483
        // global error handling
3484

3485
        $global_error_handler = Setup::getGlobalErrorHandler();
7✔
3486
        if ($global_error_handler) {
7✔
3487
            if ($global_error_handler instanceof LoggerInterface) {
×
3488
                // PSR-3 https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
3489
                $global_error_handler->error($error);
×
3490
            } elseif (\is_callable($global_error_handler)) {
×
3491
                // error callback
3492
                /** @noinspection VariableFunctionsUsageInspection */
3493
                \call_user_func($global_error_handler, $error);
×
3494
            }
3495
        }
3496

3497
        // local error handling
3498

3499
        if (isset($this->error_handler)) {
7✔
3500
            if ($this->error_handler instanceof LoggerInterface) {
3✔
3501
                // PSR-3 https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
3502
                $this->error_handler->error($error);
×
3503
            } elseif (\is_callable($this->error_handler)) {
3✔
3504
                // error callback
3505
                \call_user_func($this->error_handler, $error);
3✔
3506
            }
3507
        } else {
3508
            /** @noinspection ForgottenDebugOutputInspection */
3509
            \error_log($error);
4✔
3510
        }
3511
    }
3512

3513
    /**
3514
     * Turn payload from structured data into a string based on the current Mime type.
3515
     * This uses the auto_serialize option to determine it's course of action.
3516
     *
3517
     * See serialize method for more.
3518
     *
3519
     * Added in support for custom payload serializers.
3520
     * The serialize_payload_method stuff still holds true though.
3521
     *
3522
     * @param array<int|string, mixed>|string $payload
3523
     *
3524
     * @return mixed
3525
     *
3526
     * @see Request::registerPayloadSerializer()
3527
     */
3528
    private function _serializePayload($payload)
3529
    {
3530
        if (empty($payload)) {
18✔
3531
            return '';
×
3532
        }
3533

3534
        if ($this->serialize_payload_method === static::SERIALIZE_PAYLOAD_NEVER) {
18✔
3535
            return $payload;
1✔
3536
        }
3537

3538
        // When we are in "smart" mode, don't serialize strings/scalars, assume they are already serialized.
3539
        if (
3540
            $this->serialize_payload_method === static::SERIALIZE_PAYLOAD_SMART
17✔
3541
            &&
3542
            \is_array($payload)
17✔
3543
            &&
3544
            \count($payload) === 1
17✔
3545
            &&
3546
            \array_keys($payload)[0] === 0
17✔
3547
            &&
3548
            \is_scalar($payload_first = \array_values($payload)[0])
17✔
3549
        ) {
3550
            return $payload_first;
5✔
3551
        }
3552

3553
        // Use a custom serializer if one is registered for this mime type.
3554
        $issetContentType = isset($this->payload_serializers[$this->content_type]);
12✔
3555
        if (
3556
            $issetContentType
12✔
3557
            ||
3558
            isset($this->payload_serializers['*'])
12✔
3559
        ) {
3560
            if ($issetContentType) {
×
3561
                $key = $this->content_type;
×
3562
            } else {
3563
                $key = '*';
×
3564
            }
3565

3566
            return \call_user_func($this->payload_serializers[$key], $payload);
×
3567
        }
3568

3569
        return Setup::setupGlobalMimeType($this->content_type)->serialize($payload);
12✔
3570
    }
3571

3572
    /**
3573
     * Set the body of the request.
3574
     *
3575
     * @param mixed|null  $payload
3576
     * @param mixed|null  $key
3577
     * @param string|null $mimeType currently, sets the sends AND expects mime type although this
3578
     *                              behavior may change in the next minor release (as it is a potential breaking change)
3579
     *
3580
     * @return static
3581
     */
3582
    private function _setBody($payload, $key = null, ?string $mimeType = null): self
3583
    {
3584
        $this->_withMimeType($mimeType);
44✔
3585

3586
        if (!empty($payload)) {
44✔
3587
            if (\is_array($payload)) {
33✔
3588
                foreach ($payload as $keyInner => $valueInner) {
17✔
3589
                    $this->_setBody($valueInner, $keyInner, $mimeType);
17✔
3590
                }
3591

3592
                return $this;
17✔
3593
            }
3594

3595
            if ($payload instanceof StreamInterface) {
33✔
3596
                $this->payload = (string) $payload;
3✔
3597
            } elseif ($key === null) {
30✔
3598
                if (\is_string($this->payload)) {
13✔
3599
                    $tmpPayload = $this->payload;
×
3600
                    $this->payload = [];
×
3601
                    $this->payload[] = $tmpPayload;
×
3602
                }
3603

3604
                $this->payload[] = $payload;
13✔
3605
            } else {
3606
                if (\is_string($this->payload)) {
17✔
3607
                    $tmpPayload = $this->payload;
×
3608
                    $this->payload = [];
×
3609
                    $this->payload[] = $tmpPayload;
×
3610
                }
3611

3612
                $this->payload[$key] = $payload;
17✔
3613
            }
3614
        }
3615

3616
        // Don't call _serializePayload yet.
3617
        // Wait until we actually send off the request to convert payload to string.
3618
        // At that time, the `serialized_payload` is set accordingly.
3619

3620
        return $this;
44✔
3621
    }
3622

3623
    /**
3624
     * Set the defaults on a newly instantiated object
3625
     * Doesn't copy variables prefixed with _
3626
     *
3627
     * @return static
3628
     */
3629
    private function _setDefaultsFromTemplate(): self
3630
    {
3631
        if ($this->template !== null) {
381✔
3632
            if (\function_exists('gzdecode')) {
381✔
3633
                $this->template->content_encoding = 'gzip';
381✔
3634
            } elseif (\function_exists('gzinflate')) {
×
3635
                $this->template->content_encoding = 'deflate';
×
3636
            }
3637

3638
            foreach ($this->template as $k => $v) {
381✔
3639
                if ($k[0] !== '_') {
381✔
3640
                    $this->{$k} = $v;
381✔
3641
                }
3642
            }
3643
        }
3644

3645
        return $this;
381✔
3646
    }
3647

3648
    /**
3649
     * Set the method.  Shouldn't be called often as the preferred syntax
3650
     * for instantiation is the method specific factory methods.
3651
     *
3652
     * @param string|null $method
3653
     *
3654
     * @return static
3655
     */
3656
    private function _setMethod($method): self
3657
    {
3658
        if (empty($method)) {
381✔
3659
            return $this;
51✔
3660
        }
3661

3662
        if (!\in_array($method, Http::allMethods(), true)) {
381✔
3663
            throw new RequestException($this, 'Unknown HTTP method: \'' . \strip_tags($method) . '\'');
1✔
3664
        }
3665

3666
        $this->method = $method;
381✔
3667

3668
        return $this;
381✔
3669
    }
3670

3671
    /**
3672
     * Do we strictly enforce SSL verification?
3673
     *
3674
     * @param bool $strict
3675
     *
3676
     * @return static
3677
     */
3678
    private function _strictSSL($strict): self
3679
    {
3680
        $new = clone $this;
381✔
3681

3682
        $new->strict_ssl = $strict;
381✔
3683

3684
        return $new;
381✔
3685
    }
3686

3687
    /**
3688
     * @return void
3689
     */
3690
    private function _updateHostFromUri()
3691
    {
3692
        if ($this->uri === null) {
335✔
3693
            return;
×
3694
        }
3695

3696
        if ($this->uri_cache === \serialize($this->uri)) {
335✔
3697
            return;
3✔
3698
        }
3699

3700
        $host = $this->uri->getHost();
335✔
3701

3702
        if ($host === '') {
335✔
3703
            return;
15✔
3704
        }
3705

3706
        $port = $this->uri->getPort();
322✔
3707
        if ($port !== null) {
322✔
3708
            $host .= ':' . $port;
68✔
3709
        }
3710

3711
        // Ensure Host is the first header.
3712
        // See: http://tools.ietf.org/html/rfc7230#section-5.4
3713
        $this->headers = new Headers(['Host' => [$host]] + $this->withoutHeader('Host')->getHeaders());
322✔
3714

3715
        $this->uri_cache = \serialize($this->uri);
322✔
3716
    }
3717

3718
    /**
3719
     * @param string|null $mime     use a constant from Mime::*
3720
     * @param string|null $fallback use a constant from Mime::*
3721
     *
3722
     * @return static
3723
     */
3724
    private function _withContentType($mime, ?string $fallback = null): self
3725
    {
3726
        if (empty($mime) && empty($fallback)) {
381✔
3727
            return $this;
×
3728
        }
3729

3730
        if (empty($mime)) {
381✔
3731
            $mime = $fallback;
381✔
3732
        }
3733

3734
        $this->content_type = Mime::getFullMime($mime);
381✔
3735

3736
        if ($this->isUpload()) {
381✔
3737
            $this->neverSerializePayload();
5✔
3738
        }
3739

3740
        return $this;
381✔
3741
    }
3742

3743
    /**
3744
     * @param string|null $mime     use a constant from Mime::*
3745
     * @param string|null $fallback use a constant from Mime::*
3746
     *
3747
     * @return static
3748
     */
3749
    private function _withExpectedType($mime, ?string $fallback = null): self
3750
    {
3751
        if (empty($mime) && empty($fallback)) {
381✔
3752
            return $this;
×
3753
        }
3754

3755
        if (empty($mime)) {
381✔
3756
            $mime = $fallback;
381✔
3757
        }
3758

3759
        $this->expected_type = Mime::getFullMime($mime);
381✔
3760

3761
        return $this;
381✔
3762
    }
3763

3764
    /**
3765
     * Helper function to set the Content type and Expected as same in one swoop.
3766
     *
3767
     * @param string|null $mime mime type to use for content type and expected return type
3768
     *
3769
     * @return static
3770
     */
3771
    private function _withMimeType($mime): self
3772
    {
3773
        if (empty($mime)) {
303✔
3774
            return $this;
248✔
3775
        }
3776

3777
        $this->expected_type = Mime::getFullMime($mime);
59✔
3778
        $this->content_type = $this->expected_type;
59✔
3779

3780
        if ($this->isUpload()) {
59✔
3781
            $this->neverSerializePayload();
1✔
3782
        }
3783

3784
        return $this;
59✔
3785
    }
3786

3787
    /**
3788
     * @param UriInterface $uri
3789
     * @param bool         $preserveHost
3790
     *
3791
     * @return static
3792
     */
3793
    private function _withUri(UriInterface $uri, $preserveHost = false): self
3794
    {
3795
        if ($this->uri === $uri) {
335✔
3796
            return $this;
2✔
3797
        }
3798

3799
        $this->uri = $uri;
335✔
3800

3801
        if (!$preserveHost) {
335✔
3802
            $this->_updateHostFromUri();
335✔
3803
        }
3804

3805
        return $this;
335✔
3806
    }
3807
}
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