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

voku / httpful / 25187974306

30 Apr 2026 08:34PM UTC coverage: 90.483% (+0.6%) from 89.902%
25187974306

push

github

web-flow
Merge pull request #26 from voku/php8

[+]: PHP 8.0+ rework

368 of 407 new or added lines in 7 files covered. (90.42%)

1 existing line in 1 file now uncovered.

2548 of 2816 relevant lines covered (90.48%)

49.01 hits per line

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

87.65
/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 static(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
            /** @var mixed $value */
1060
            $value = $this->headers->offsetGet($name);
20✔
1061

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

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

1070
            return $value;
20✔
1071
        }
1072

1073
        return [];
2✔
1074
    }
1075

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

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

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

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

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

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

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

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

1163
        return $target;
8✔
1164
    }
1165

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

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

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

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

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

1226
        $new = clone $this;
9✔
1227

1228
        if (!\is_array($value)) {
9✔
1229
            $value = [$value];
9✔
1230
        }
1231

1232
        if ($new->headers->offsetExists($headerName)) {
9✔
1233
            /** @var mixed $currentValues */
1234
            $currentValues = $new->headers->offsetGet($headerName);
4✔
1235
            if (!\is_array($currentValues)) {
4✔
NEW
1236
                $currentValues = [$currentValues];
×
1237
            }
1238
            $new->headers->forceSet($headerName, \array_merge_recursive($currentValues, $value));
4✔
1239
        } else {
1240
            $new->headers->forceSet($headerName, $value);
7✔
1241
        }
1242

1243
        return $new;
9✔
1244
    }
1245

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

1265
        return (clone $this)->_setBody($stream, null);
3✔
1266
    }
1267

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

1289
        if (!\is_array($value)) {
25✔
1290
            $value = [$value];
22✔
1291
        }
1292

1293
        $new->headers->forceSet($name, $value);
25✔
1294

1295
        return $new;
24✔
1296
    }
1297

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

1320
        $new->_setMethod($method);
3✔
1321

1322
        return $new;
3✔
1323
    }
1324

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

1344
        $new->protocol_version = $version;
6✔
1345
        $new->curl_http_version = null;
6✔
1346

1347
        switch ((string) $version) {
6✔
1348
            case Http::HTTP_1_0:
1349
                $new->curl_http_version = \CURL_HTTP_VERSION_1_0;
2✔
1350

1351
                break;
2✔
1352
            case Http::HTTP_1_1:
4✔
1353
                $new->curl_http_version = \CURL_HTTP_VERSION_1_1;
1✔
1354

1355
                break;
1✔
1356
            case Http::HTTP_2_0:
4✔
1357
                $new->curl_http_version = \CURL_HTTP_VERSION_2_0;
2✔
1358

1359
                break;
2✔
1360
            case Http::HTTP_3:
2✔
1361
                $new->curl_http_version = 'CURL_HTTP_VERSION_3';
1✔
1362

1363
                break;
1✔
1364
        }
1365

1366
        return $new;
6✔
1367
    }
1368

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

1377
    /**
1378
     * @return static
1379
     */
1380
    public function withHttp2PriorKnowledge(): self
1381
    {
1382
        return $this->_withCurlHttpVersion(Http::HTTP_2_0, 'CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE');
1✔
1383
    }
1384

1385
    /**
1386
     * @return static
1387
     */
1388
    public function withHttp3(): self
1389
    {
NEW
1390
        return $this->_withCurlHttpVersion(Http::HTTP_3, 'CURL_HTTP_VERSION_3');
×
1391
    }
1392

1393
    /**
1394
     * @return static
1395
     */
1396
    public function withHttp3Only(): self
1397
    {
1398
        return $this->_withCurlHttpVersion(Http::HTTP_3, 'CURL_HTTP_VERSION_3ONLY');
1✔
1399
    }
1400

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

1426
        $new = clone $this;
3✔
1427

1428
        if ($new->uri !== null) {
3✔
1429
            $new->_withUri($new->uri->withPath($requestTarget));
2✔
1430
        }
1431

1432
        return $new;
3✔
1433
    }
1434

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

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

1489
        $new->headers->forceUnset($name);
322✔
1490

1491
        return $new;
322✔
1492
    }
1493

1494
    /**
1495
     * @return string
1496
     */
1497
    public function getContentType(): string
1498
    {
1499
        return $this->content_type;
10✔
1500
    }
1501

1502
    /**
1503
     * @return callable|LoggerInterface|null
1504
     */
1505
    public function getErrorHandler()
1506
    {
1507
        return $this->error_handler;
2✔
1508
    }
1509

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

1518
    /**
1519
     * @return string
1520
     */
1521
    public function getHttpMethod(): string
1522
    {
1523
        return $this->method;
4✔
1524
    }
1525

1526
    /**
1527
     * @return \ArrayObject<string, mixed>
1528
     */
1529
    public function getIterator(): \ArrayObject
1530
    {
1531
        // init
1532
        $elements = new \ArrayObject();
381✔
1533

1534
        foreach (\get_object_vars($this) as $f => $v) {
381✔
1535
            $elements[$f] = $v;
381✔
1536
        }
1537

1538
        return $elements;
381✔
1539
    }
1540

1541
    /**
1542
     * @return callable|null
1543
     */
1544
    public function getParseCallback()
1545
    {
1546
        return $this->parse_callback;
3✔
1547
    }
1548

1549
    /**
1550
     * @return array<int|string, \CURLFile|string>
1551
     */
1552
    public function getPayload(): array
1553
    {
1554
        return \is_string($this->payload) ? [$this->payload] : $this->payload;
2✔
1555
    }
1556

1557
    /**
1558
     * @return string
1559
     */
1560
    public function getRawHeaders(): string
1561
    {
1562
        return $this->raw_headers;
8✔
1563
    }
1564

1565
    /**
1566
     * @return callable[]
1567
     */
1568
    public function getSendCallback(): array
1569
    {
1570
        return $this->send_callbacks;
5✔
1571
    }
1572

1573
    /**
1574
     * @return int
1575
     */
1576
    public function getSerializePayloadMethod(): int
1577
    {
1578
        return $this->serialize_payload_method;
3✔
1579
    }
1580

1581
    /**
1582
     * @return mixed|null
1583
     */
1584
    public function getSerializedPayload()
1585
    {
1586
        return $this->serialized_payload;
4✔
1587
    }
1588

1589
    /**
1590
     * @return string
1591
     */
1592
    public function getUriString(): string
1593
    {
1594
        return (string) $this->uri;
10✔
1595
    }
1596

1597
    /**
1598
     * Is this request setup for basic auth?
1599
     *
1600
     * @return bool
1601
     */
1602
    public function hasBasicAuth(): bool
1603
    {
1604
        return $this->password && $this->username;
106✔
1605
    }
1606

1607
    /**
1608
     * @return bool has the internal curl (non-multi) request been initialized?
1609
     */
1610
    public function hasBeenInitialized(): bool
1611
    {
1612
        if (!$this->curl) {
105✔
1613
            return false;
×
1614
        }
1615

1616
        return $this->curl->getCurl() !== false;
105✔
1617
    }
1618

1619
    /**
1620
     * @return bool has the internal curl (multi) request been initialized?
1621
     */
1622
    public function hasBeenInitializedMulti(): bool
1623
    {
1624
        if (!$this->curlMulti) {
1✔
1625
            return false;
1✔
1626
        }
1627

NEW
1628
        return $this->curlMulti->getMultiCurl() !== null;
×
1629
    }
1630

1631
    /**
1632
     * @return bool is this request setup for client side cert?
1633
     */
1634
    public function hasClientSideCert(): bool
1635
    {
1636
        return $this->ssl_cert && $this->ssl_key;
106✔
1637
    }
1638

1639
    /**
1640
     * @return bool does the request have a connection timeout?
1641
     */
1642
    public function hasConnectionTimeout(): bool
1643
    {
1644
        return isset($this->connection_timeout);
104✔
1645
    }
1646

1647
    /**
1648
     * Is this request setup for digest auth?
1649
     *
1650
     * @return bool
1651
     */
1652
    public function hasDigestAuth(): bool
1653
    {
1654
        return $this->password
3✔
1655
               &&
3✔
1656
               $this->username
3✔
1657
               &&
3✔
1658
               $this->additional_curl_opts[\CURLOPT_HTTPAUTH] === \CURLAUTH_DIGEST;
3✔
1659
    }
1660

1661
    /**
1662
     * @return bool
1663
     */
1664
    public function hasParseCallback(): bool
1665
    {
1666
        return $this->parse_callback !== null;
85✔
1667
    }
1668

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

1685
    /**
1686
     * @return bool does the request have a timeout?
1687
     */
1688
    public function hasTimeout(): bool
1689
    {
1690
        return isset($this->timeout);
106✔
1691
    }
1692

1693
    /**
1694
     * HTTP Method Head
1695
     *
1696
     * @param string|UriInterface $uri
1697
     *
1698
     * @return static
1699
     */
1700
    public static function head($uri): self
1701
    {
1702
        if ($uri instanceof UriInterface) {
8✔
1703
            $uri = (string) $uri;
×
1704
        }
1705

1706
        /** @var static $request */
1707
        $request = new static(Http::HEAD);
8✔
1708
        $request = $request->withUriFromString($uri);
8✔
1709

1710
        return $request->withMimeType(Mime::PLAIN);
8✔
1711
    }
1712

1713
    /**
1714
     * @see Request::close()
1715
     *
1716
     * @return void
1717
     */
1718
    public function initializeMulti()
1719
    {
1720
        if (!$this->curlMulti || $this->hasBeenInitializedMulti()) {
28✔
1721
            $this->curlMulti = new MultiCurl();
28✔
1722
        }
1723
    }
1724

1725
    /**
1726
     * @see Request::close()
1727
     *
1728
     * @return void
1729
     */
1730
    public function initialize()
1731
    {
1732
        if (!$this->curl || !$this->hasBeenInitialized()) {
381✔
1733
            $this->curl = new Curl();
381✔
1734
        }
1735
    }
1736

1737
    /**
1738
     * @return bool
1739
     */
1740
    public function isAutoParse(): bool
1741
    {
1742
        return $this->auto_parse;
86✔
1743
    }
1744

1745
    /**
1746
     * @return bool
1747
     */
1748
    public function isJson(): bool
1749
    {
1750
        return $this->content_type === Mime::JSON;
4✔
1751
    }
1752

1753
    /**
1754
     * @return bool
1755
     */
1756
    public function isStrictSSL(): bool
1757
    {
1758
        return $this->strict_ssl;
6✔
1759
    }
1760

1761
    /**
1762
     * @return bool
1763
     */
1764
    public function isUpload(): bool
1765
    {
1766
        return $this->content_type === Mime::UPLOAD;
381✔
1767
    }
1768

1769
    /**
1770
     * @return static
1771
     *
1772
     * @see Request::serializePayloadMode()
1773
     */
1774
    public function neverSerializePayload(): self
1775
    {
1776
        return $this->serializePayloadMode(static::SERIALIZE_PAYLOAD_NEVER);
9✔
1777
    }
1778

1779
    /**
1780
     * HTTP Method Options
1781
     *
1782
     * @param string|UriInterface $uri
1783
     *
1784
     * @return static
1785
     */
1786
    public static function options($uri): self
1787
    {
1788
        if ($uri instanceof UriInterface) {
4✔
1789
            $uri = (string) $uri;
×
1790
        }
1791

1792
        /** @var static $request */
1793
        $request = new static(Http::OPTIONS);
4✔
1794

1795
        return $request->withUriFromString($uri);
4✔
1796
    }
1797

1798
    /**
1799
     * HTTP Method Patch
1800
     *
1801
     * @param string|UriInterface $uri
1802
     * @param mixed               $payload data to send in body of request
1803
     * @param string              $mime    MIME to use for Content-Type
1804
     *
1805
     * @return static
1806
     */
1807
    public static function patch($uri, $payload = null, ?string $mime = null): self
1808
    {
1809
        if ($uri instanceof UriInterface) {
5✔
1810
            $uri = (string) $uri;
×
1811
        }
1812

1813
        /** @var static $request */
1814
        $request = new static(Http::PATCH);
5✔
1815
        $request = $request->withUriFromString($uri);
5✔
1816

1817
        return $request->_setBody($payload, null, $mime);
5✔
1818
    }
1819

1820
    /**
1821
     * HTTP Method Post
1822
     *
1823
     * @param string|UriInterface $uri
1824
     * @param mixed               $payload data to send in body of request
1825
     * @param string              $mime    MIME to use for Content-Type
1826
     *
1827
     * @return static
1828
     */
1829
    public static function post($uri, $payload = null, ?string $mime = null): self
1830
    {
1831
        if ($uri instanceof UriInterface) {
24✔
1832
            $uri = (string) $uri;
×
1833
        }
1834

1835
        /** @var static $request */
1836
        $request = new static(Http::POST);
24✔
1837
        $request = $request->withUriFromString($uri);
24✔
1838

1839
        return $request->_setBody($payload, null, $mime);
24✔
1840
    }
1841

1842
    /**
1843
     * HTTP Method Put
1844
     *
1845
     * @param string|UriInterface $uri
1846
     * @param mixed               $payload data to send in body of request
1847
     * @param string              $mime    MIME to use for Content-Type
1848
     *
1849
     * @return static
1850
     */
1851
    public static function put($uri, $payload = null, ?string $mime = null): self
1852
    {
1853
        if ($uri instanceof UriInterface) {
4✔
1854
            $uri = (string) $uri;
×
1855
        }
1856

1857
        /** @var static $request */
1858
        $request = new static(Http::PUT);
4✔
1859
        $request = $request->withUriFromString($uri);
4✔
1860

1861
        return $request->_setBody($payload, null, $mime);
4✔
1862
    }
1863

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

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

1883
        return $new;
2✔
1884
    }
1885

1886
    /**
1887
     * @return void
1888
     */
1889
    public function reset()
1890
    {
1891
        $this->headers = new Headers();
1✔
1892

1893
        $this->close();
1✔
1894
        $this->initialize();
1✔
1895
    }
1896

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

1918
        if ($onSuccessCallback !== null) {
27✔
1919
            $this->curlMulti->success(
5✔
1920
                static function (Curl $instance) use ($onSuccessCallback) {
5✔
1921
                    if ($instance->request instanceof self) {
3✔
1922
                        $response = $instance->request->_buildResponse($instance->rawResponse, $instance);
3✔
1923
                    } else {
1924
                        $response = $instance->rawResponse;
×
1925
                    }
1926

1927
                    $onSuccessCallback(
3✔
1928
                        $response,
3✔
1929
                        $instance->request,
3✔
1930
                        $instance
3✔
1931
                    );
3✔
1932
                }
5✔
1933
            );
5✔
1934
        }
1935

1936
        if ($onCompleteCallback !== null) {
27✔
1937
            $this->curlMulti->complete(
2✔
1938
                static function (Curl $instance) use ($onCompleteCallback) {
2✔
1939
                    if ($instance->request instanceof self) {
×
1940
                        $response = $instance->request->_buildResponse($instance->rawResponse, $instance);
×
1941
                    } else {
1942
                        $response = $instance->rawResponse;
×
1943
                    }
1944

1945
                    $onCompleteCallback(
×
1946
                        $response,
×
1947
                        $instance->request,
×
1948
                        $instance
×
1949
                    );
×
1950
                }
2✔
1951
            );
2✔
1952
        }
1953

1954
        if ($onBeforeSendCallback !== null) {
27✔
1955
            $this->curlMulti->beforeSend(
×
1956
                static function (Curl $instance) use ($onBeforeSendCallback) {
×
1957
                    if ($instance->request instanceof self) {
×
1958
                        $response = $instance->request->_buildResponse($instance->rawResponse, $instance);
×
1959
                    } else {
1960
                        $response = $instance->rawResponse;
×
1961
                    }
1962

1963
                    $onBeforeSendCallback(
×
1964
                        $response,
×
1965
                        $instance->request,
×
1966
                        $instance
×
1967
                    );
×
1968
                }
×
1969
            );
×
1970
        }
1971

1972
        if ($onErrorCallback !== null) {
27✔
1973
            $this->curlMulti->error(
×
1974
                static function (Curl $instance) use ($onErrorCallback) {
×
1975
                    if ($instance->request instanceof self) {
×
1976
                        $response = $instance->request->_buildResponse($instance->rawResponse, $instance);
×
1977
                    } else {
1978
                        $response = $instance->rawResponse;
×
1979
                    }
1980

1981
                    $onErrorCallback(
×
1982
                        $response,
×
1983
                        $instance->request,
×
1984
                        $instance
×
1985
                    );
×
1986
                }
×
1987
            );
×
1988
        }
1989

1990
        return $this->curlMulti;
27✔
1991
    }
1992

1993
    /**
1994
     * Actually send off the request, and parse the response.
1995
     *
1996
     * @throws NetworkErrorException when unable to parse or communicate w server
1997
     *
1998
     * @return Response
1999
     */
2000
    public function send(): Response
2001
    {
2002
        $this->_curlPrep();
31✔
2003
        $curl = $this->curl;
31✔
2004
        if ($curl === null) {
31✔
NEW
2005
            throw new NetworkErrorException('Unable to initialize cURL.');
×
2006
        }
2007

2008
        $result = $curl->exec();
31✔
2009

2010
        if (
2011
            $result === false
31✔
2012
            &&
2013
            $this->retry_by_possible_encoding_error
31✔
2014
        ) {
2015
            // Possibly a gzip issue makes curl unhappy.
2016
            if (
NEW
2017
                \in_array($this->curl->errorCode, [\CURLE_WRITE_ERROR, \CURLE_BAD_CONTENT_ENCODING], true)
×
2018
            ) {
2019
                // Docs say 'identity,' but 'none' seems to work (sometimes?).
2020
                $this->curl->setOpt(\CURLOPT_ENCODING, 'none');
×
2021

2022
                $result = $this->curl->exec();
×
2023

2024
                if ($result === false) {
×
2025
                    if (
NEW
2026
                        \in_array($this->curl->errorCode, [\CURLE_WRITE_ERROR, \CURLE_BAD_CONTENT_ENCODING], true)
×
2027
                    ) {
2028
                        $this->curl->setOpt(\CURLOPT_ENCODING, 'identity');
×
2029

2030
                        $result = $this->curl->exec();
×
2031
                    }
2032
                }
2033
            }
2034
        }
2035

2036
        if (!$this->keep_alive) {
31✔
2037
            $this->close();
×
2038
        }
2039

2040
        return $this->_buildResponse($result);
31✔
2041
    }
2042

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

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

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

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

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

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

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

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

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

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

2123
    /**
2124
     * @return static
2125
     */
2126
    public function sendsXml(): self
2127
    {
2128
        return $this->withContentType(Mime::XML);
1✔
2129
    }
2130

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

2158
        return $this;
9✔
2159
    }
2160

2161
    /**
2162
     * This method is the default behavior
2163
     *
2164
     * @return static
2165
     *
2166
     * @see Request::serializePayloadMode()
2167
     */
2168
    public function smartSerializePayload(): self
2169
    {
2170
        return $this->serializePayloadMode(static::SERIALIZE_PAYLOAD_SMART);
1✔
2171
    }
2172

2173
    /**
2174
     * Specify a HTTP timeout
2175
     *
2176
     * @param float|int $timeout seconds to timeout the HTTP call
2177
     *
2178
     * @return static
2179
     */
2180
    public function withTimeout($timeout): self
2181
    {
2182
        if (!\preg_match('/^\d+(\.\d+)?/', (string) $timeout)) {
9✔
2183
            throw new \InvalidArgumentException(
1✔
2184
                'Invalid timeout provided: ' . \var_export($timeout, true)
1✔
2185
            );
1✔
2186
        }
2187

2188
        $new = clone $this;
8✔
2189

2190
        $new->timeout = $timeout;
8✔
2191

2192
        return $new;
8✔
2193
    }
2194

2195
    /**
2196
     * @param int|string $maximum_number_of_retries
2197
     *
2198
     * @return static
2199
     */
2200
    public function withRetry($maximum_number_of_retries): self
2201
    {
2202
        if (!\preg_match('/^\d+$/', (string) $maximum_number_of_retries)) {
10✔
2203
            throw new \InvalidArgumentException(
1✔
2204
                'Invalid retry count provided: ' . \var_export($maximum_number_of_retries, true)
1✔
2205
            );
1✔
2206
        }
2207

2208
        $new = clone $this;
9✔
2209
        $new->retry = (int) $maximum_number_of_retries;
9✔
2210

2211
        return $new;
9✔
2212
    }
2213

2214
    /**
2215
     * @param float|int $delay seconds between retry attempts
2216
     *
2217
     * @return static
2218
     */
2219
    public function withRetryDelay($delay): self
2220
    {
2221
        $new = clone $this;
9✔
2222
        $new->retry_delay = $this->_normalizeDurationValue($delay, 'retry delay');
9✔
2223

2224
        return $new;
8✔
2225
    }
2226

2227
    /**
2228
     * @param float|int $max_time overall retry budget in seconds
2229
     *
2230
     * @return static
2231
     */
2232
    public function withRetryMaxTime($max_time): self
2233
    {
2234
        $new = clone $this;
4✔
2235
        $new->retry_max_time = $this->_normalizeDurationValue($max_time, 'retry max time');
4✔
2236

2237
        return $new;
3✔
2238
    }
2239

2240
    /**
2241
     * @return static
2242
     */
2243
    public function withRetryAllErrors(bool $retry_all_errors = true): self
2244
    {
2245
        $new = clone $this;
2✔
2246
        $new->retry_all_errors = $retry_all_errors;
2✔
2247

2248
        return $new;
2✔
2249
    }
2250

2251
    /**
2252
     * @return static
2253
     */
2254
    public function withRetryConnectionRefused(bool $retry_connection_refused = true): self
2255
    {
2256
        $new = clone $this;
2✔
2257
        $new->retry_connection_refused = $retry_connection_refused;
2✔
2258

2259
        return $new;
2✔
2260
    }
2261

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

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

2323
    /**
2324
     * @param string $name
2325
     * @param string $value
2326
     *
2327
     * @return static
2328
     */
2329
    public function withAddedCookie(string $name, string $value): self
2330
    {
2331
        return $this->withAddedHeader('Cookie', "{$name}={$value}");
3✔
2332
    }
2333

2334
    /**
2335
     * @param array<string,string> $files
2336
     *
2337
     * @return static
2338
     */
2339
    public function withAttachment($files): self
2340
    {
2341
        $new = clone $this;
1✔
2342

2343
        $fInfo = \finfo_open(\FILEINFO_MIME_TYPE);
1✔
2344
        if ($fInfo === false) {
1✔
2345
            /** @noinspection ForgottenDebugOutputInspection */
NEW
2346
            \error_log('finfo_open() did not work');
×
2347

2348
            return $new;
×
2349
        }
2350

2351
        foreach ($files as $key => $file) {
1✔
2352
            $mimeType = \finfo_file($fInfo, $file);
1✔
2353
            if ($mimeType !== false) {
1✔
2354
                if (\is_string($new->payload)) {
1✔
2355
                    $new->payload = []; // reset
×
2356
                }
2357
                $new->payload[$key] = \curl_file_create($file, $mimeType, \basename($file));
1✔
2358
            }
2359
        }
2360

2361
        \finfo_close($fInfo);
1✔
2362

2363
        return $new->_withContentType(Mime::UPLOAD);
1✔
2364
    }
2365

2366
    /**
2367
     * User Basic Auth.
2368
     *
2369
     * Only use when over SSL/TSL/HTTPS.
2370
     *
2371
     * @param string $username
2372
     * @param string $password
2373
     *
2374
     * @return static
2375
     */
2376
    public function withBasicAuth($username, $password): self
2377
    {
2378
        $new = clone $this;
13✔
2379
        $new->username = $username;
13✔
2380
        $new->password = $password;
13✔
2381

2382
        return $new;
13✔
2383
    }
2384

2385
    /**
2386
     * @param string $token
2387
     *
2388
     * @return static
2389
     */
2390
    public function withBearerToken(string $token): self
2391
    {
2392
        return $this->withHeader('Authorization', 'Bearer ' . $token);
1✔
2393
    }
2394

2395
    /**
2396
     * @param array<string|int, mixed> $body
2397
     *
2398
     * @return static
2399
     */
2400
    public function withBodyFromArray(array $body)
2401
    {
2402
        return $this->_setBody($body, null);
3✔
2403
    }
2404

2405
    /**
2406
     * @param string $body
2407
     *
2408
     * @return static
2409
     */
2410
    public function withBodyFromString(string $body)
2411
    {
2412
        $stream = Http::stream($body);
10✔
2413

2414
        return $this->_setBody($stream->getContents(), null);
10✔
2415
    }
2416

2417
    /**
2418
     * Specify a HTTP connection timeout
2419
     *
2420
     * @param float|int $connection_timeout seconds to timeout the HTTP connection
2421
     *
2422
     * @throws \InvalidArgumentException
2423
     *
2424
     * @return static
2425
     */
2426
    public function withConnectionTimeoutInSeconds($connection_timeout): self
2427
    {
2428
        if (!\preg_match('/^\d+(\.\d+)?/', (string) $connection_timeout)) {
7✔
2429
            throw new \InvalidArgumentException(
1✔
2430
                'Invalid connection timeout provided: ' . \var_export($connection_timeout, true)
1✔
2431
            );
1✔
2432
        }
2433

2434
        $new = clone $this;
6✔
2435

2436
        $new->connection_timeout = $connection_timeout;
6✔
2437

2438
        return $new;
6✔
2439
    }
2440

2441
    /**
2442
     * @param string $ca_bundle_path
2443
     *
2444
     * @return static
2445
     */
2446
    public function withCaBundle(string $ca_bundle_path): self
2447
    {
2448
        return $this->_withNamedCurlOption('CURLOPT_CAINFO', $ca_bundle_path);
1✔
2449
    }
2450

2451
    /**
2452
     * @param string $ca_path
2453
     *
2454
     * @return static
2455
     */
2456
    public function withCaPath(string $ca_path): self
2457
    {
2458
        return $this->_withNamedCurlOption('CURLOPT_CAPATH', $ca_path);
1✔
2459
    }
2460

2461
    /**
2462
     * @param string     $minimum_version
2463
     * @param string|int $maximum_version
2464
     *
2465
     * @return static
2466
     */
2467
    public function withTlsVersion($minimum_version, $maximum_version = null): self
2468
    {
2469
        [$minimum_option, $minimum_rank] = $this->_normalizeTlsVersionOption($minimum_version, false);
4✔
2470
        $ssl_version = $minimum_option;
3✔
2471

2472
        if ($maximum_version !== null) {
3✔
2473
            [$maximum_option, $maximum_rank] = $this->_normalizeTlsVersionOption($maximum_version, true);
2✔
2474

2475
            if (
2476
                $minimum_rank !== null
2✔
2477
                &&
2478
                $maximum_rank !== null
2✔
2479
                &&
2480
                $minimum_rank > $maximum_rank
2✔
2481
            ) {
2482
                throw new \InvalidArgumentException('The minimum TLS version cannot be greater than the maximum TLS version.');
1✔
2483
            }
2484

2485
            $ssl_version |= $maximum_option;
1✔
2486
        }
2487

2488
        return $this->_withNamedCurlOption('CURLOPT_SSLVERSION', $ssl_version);
2✔
2489
    }
2490

2491
    /**
2492
     * @param string $pinned_public_key
2493
     *
2494
     * @return static
2495
     */
2496
    public function withPinnedPublicKey(string $pinned_public_key): self
2497
    {
2498
        return $this->_withNamedCurlOption('CURLOPT_PINNEDPUBLICKEY', $pinned_public_key);
1✔
2499
    }
2500

2501
    /**
2502
     * @param string $cache_control
2503
     *                              <p>e.g. 'no-cache', 'public', ...</p>
2504
     *
2505
     * @return static
2506
     */
2507
    public function withCacheControl(string $cache_control): self
2508
    {
2509
        $new = clone $this;
6✔
2510

2511
        if (empty($cache_control)) {
6✔
2512
            return $new;
1✔
2513
        }
2514

2515
        $new->cache_control = $cache_control;
5✔
2516

2517
        return $new;
5✔
2518
    }
2519

2520
    /**
2521
     * @param string $charset
2522
     *                        <p>e.g. "UTF-8"</p>
2523
     *
2524
     * @return static
2525
     */
2526
    public function withContentCharset(string $charset): self
2527
    {
2528
        $new = clone $this;
2✔
2529

2530
        if (empty($charset)) {
2✔
2531
            return $new;
1✔
2532
        }
2533

2534
        $new->content_charset = UTF8::normalize_encoding($charset);
1✔
2535

2536
        return $new;
1✔
2537
    }
2538

2539
    /**
2540
     * @param int $port
2541
     *
2542
     * @return static
2543
     */
2544
    public function withPort(int $port): self
2545
    {
2546
        $new = clone $this;
5✔
2547

2548
        $new->port = $port;
5✔
2549
        if ($new->uri) {
5✔
2550
            $new->uri = $new->uri->withPort($port);
4✔
2551
            $new->_updateHostFromUri();
4✔
2552
        }
2553

2554
        return $new;
5✔
2555
    }
2556

2557
    /**
2558
     * @param string $encoding
2559
     *
2560
     * @return static
2561
     */
2562
    public function withContentEncoding(string $encoding): self
2563
    {
2564
        $new = clone $this;
6✔
2565

2566
        $new->content_encoding = $encoding;
6✔
2567

2568
        return $new;
6✔
2569
    }
2570

2571
    /**
2572
     * @param string|null $mime     use a constant from Mime::*
2573
     * @param string|null $fallback use a constant from Mime::*
2574
     *
2575
     * @return static
2576
     */
2577
    public function withContentType($mime, ?string $fallback = null): self
2578
    {
2579
        return (clone $this)->_withContentType($mime, $fallback);
7✔
2580
    }
2581

2582
    /**
2583
     * @return static
2584
     */
2585
    public function withContentTypeCsv(): self
2586
    {
2587
        $new = clone $this;
1✔
2588
        $new->content_type = Mime::getFullMime(Mime::CSV);
1✔
2589

2590
        return $new;
1✔
2591
    }
2592

2593
    /**
2594
     * @return static
2595
     */
2596
    public function withContentTypeForm(): self
2597
    {
2598
        $new = clone $this;
2✔
2599
        $new->content_type = Mime::getFullMime(Mime::FORM);
2✔
2600

2601
        return $new;
2✔
2602
    }
2603

2604
    /**
2605
     * @return static
2606
     */
2607
    public function withContentTypeHtml(): self
2608
    {
2609
        $new = clone $this;
1✔
2610
        $new->content_type = Mime::getFullMime(Mime::HTML);
1✔
2611

2612
        return $new;
1✔
2613
    }
2614

2615
    /**
2616
     * @return static
2617
     */
2618
    public function withContentTypeJson(): self
2619
    {
2620
        $new = clone $this;
1✔
2621
        $new->content_type = Mime::getFullMime(Mime::JSON);
1✔
2622

2623
        return $new;
1✔
2624
    }
2625

2626
    /**
2627
     * @return static
2628
     */
2629
    public function withContentTypePlain(): self
2630
    {
2631
        $new = clone $this;
1✔
2632
        $new->content_type = Mime::getFullMime(Mime::PLAIN);
1✔
2633

2634
        return $new;
1✔
2635
    }
2636

2637
    /**
2638
     * @return static
2639
     */
2640
    public function withContentTypeXml(): self
2641
    {
2642
        $new = clone $this;
1✔
2643
        $new->content_type = Mime::getFullMime(Mime::XML);
1✔
2644

2645
        return $new;
1✔
2646
    }
2647

2648
    /**
2649
     * @return static
2650
     */
2651
    public function withContentTypeYaml(): self
2652
    {
2653
        return $this->withContentType(Mime::YAML);
1✔
2654
    }
2655

2656
    /**
2657
     * @param string $name
2658
     * @param string $value
2659
     *
2660
     * @return static
2661
     */
2662
    public function withCookie(string $name, string $value): self
2663
    {
2664
        return $this->withHeader('Cookie', "{$name}={$value}");
2✔
2665
    }
2666

2667
    /**
2668
     * @param string $cookie_file
2669
     *
2670
     * @return static
2671
     */
2672
    public function withCookieFile(string $cookie_file): self
2673
    {
2674
        return $this->_withNamedCurlOption('CURLOPT_COOKIEFILE', $cookie_file);
1✔
2675
    }
2676

2677
    /**
2678
     * @param string $cookie_jar
2679
     *
2680
     * @return static
2681
     */
2682
    public function withCookieJar(string $cookie_jar): self
2683
    {
2684
        return $this->_withNamedCurlOption('CURLOPT_COOKIEJAR', $cookie_jar);
1✔
2685
    }
2686

2687
    /**
2688
     * Semi-reluctantly added this as a way to add in curl opts
2689
     * that are not otherwise accessible from the rest of the API.
2690
     *
2691
     * @param int   $curl_opt
2692
     * @param mixed $curl_opt_val
2693
     *
2694
     * @return static
2695
     */
2696
    public function withCurlOption($curl_opt, $curl_opt_val): self
2697
    {
2698
        $new = clone $this;
15✔
2699

2700
        $new->_withCurlOptionValue($curl_opt, $curl_opt_val);
15✔
2701

2702
        return $new;
15✔
2703
    }
2704

2705
    /**
2706
     * @param string $filename
2707
     *
2708
     * @return static
2709
     */
2710
    public function withAltSvcCache(string $filename = '', bool $read_only = false): self
2711
    {
2712
        $new = $this->_withNamedCurlOption('CURLOPT_ALTSVC', $filename);
1✔
2713

2714
        return $new->_withNamedCurlOption(
1✔
2715
            'CURLOPT_ALTSVC_CTRL',
1✔
2716
            $this->_buildAltSvcControlValue($read_only)
1✔
2717
        );
1✔
2718
    }
2719

2720
    /**
2721
     * @param string|null $filename
2722
     *
2723
     * @return static
2724
     */
2725
    public function withHstsCache($filename = null, bool $read_only = false): self
2726
    {
2727
        $new = $this->_withNamedCurlOption('CURLOPT_HSTS', $filename);
1✔
2728

2729
        return $new->_withNamedCurlOption(
1✔
2730
            'CURLOPT_HSTS_CTRL',
1✔
2731
            $this->_buildHstsControlValue($read_only)
1✔
2732
        );
1✔
2733
    }
2734

2735
    /**
2736
     * User Digest Auth.
2737
     *
2738
     * @param string $username
2739
     * @param string $password
2740
     *
2741
     * @return static
2742
     */
2743
    public function withDigestAuth($username, $password): self
2744
    {
2745
        $new = clone $this;
4✔
2746

2747
        $new = $new->withCurlOption(\CURLOPT_HTTPAUTH, \CURLAUTH_DIGEST);
4✔
2748

2749
        return $new->withBasicAuth($username, $password);
4✔
2750
    }
2751

2752
    /**
2753
     * Callback called to handle HTTP errors. When nothing is set, defaults
2754
     * to logging via `error_log`.
2755
     *
2756
     * @param callable|LoggerInterface|null $error_handler
2757
     *
2758
     * @return static
2759
     */
2760
    public function withErrorHandler($error_handler): self
2761
    {
2762
        $new = clone $this;
4✔
2763

2764
        $new->error_handler = $error_handler;
4✔
2765

2766
        return $new;
4✔
2767
    }
2768

2769
    /**
2770
     * @param string|null $mime     use a constant from Mime::*
2771
     * @param string|null $fallback use a constant from Mime::*
2772
     *
2773
     * @return static
2774
     */
2775
    public function withExpectedType($mime, ?string $fallback = null): self
2776
    {
2777
        return (clone $this)->_withExpectedType($mime, $fallback);
9✔
2778
    }
2779

2780
    /**
2781
     * @param string[]|string[][] $header
2782
     *
2783
     * @return static
2784
     */
2785
    public function withHeaders(array $header): self
2786
    {
2787
        $new = clone $this;
4✔
2788

2789
        foreach ($header as $name => $value) {
4✔
2790
            $new = $new->withAddedHeader($name, $value);
4✔
2791
        }
2792

2793
        return $new;
4✔
2794
    }
2795

2796
    /**
2797
     * Helper function to set the Content type and Expected as same in one swoop.
2798
     *
2799
     * @param string|null $mime
2800
     *                          <p>\Httpful\Mime::JSON, \Httpful\Mime::XML, ...</p>
2801
     *
2802
     * @return static
2803
     */
2804
    public function withMimeType($mime): self
2805
    {
2806
        return (clone $this)->_withMimeType($mime);
262✔
2807
    }
2808

2809
    /**
2810
     * @param string $username
2811
     * @param string $password
2812
     *
2813
     * @return static
2814
     */
2815
    public function withNtlmAuth($username, $password): self
2816
    {
2817
        $new = clone $this;
2✔
2818

2819
        $new = $new->withCurlOption(\CURLOPT_HTTPAUTH, \CURLAUTH_NTLM);
2✔
2820

2821
        return $new->withBasicAuth($username, $password);
2✔
2822
    }
2823

2824
    /**
2825
     * Add additional parameter to be appended to the query string.
2826
     *
2827
     * @param int|string|null $key
2828
     * @param int|string|null $value
2829
     *
2830
     * @return static
2831
     */
2832
    public function withParam($key, $value): self
2833
    {
2834
        $new = clone $this;
5✔
2835

2836
        if (
2837
            isset($key, $value)
5✔
2838
            &&
2839
            $key !== ''
5✔
2840
        ) {
2841
            $new->params[$key] = $value;
4✔
2842
        }
2843

2844
        return $new;
5✔
2845
    }
2846

2847
    /**
2848
     * Add additional parameters to be appended to the query string.
2849
     *
2850
     * Takes an associative array of key/value pairs as an argument.
2851
     *
2852
     * @param array<string|int, mixed> $params
2853
     *
2854
     * @return static this
2855
     */
2856
    public function withParams(array $params): self
2857
    {
2858
        $new = clone $this;
3✔
2859

2860
        $new->params = \array_merge($new->params, $params);
3✔
2861

2862
        return $new;
3✔
2863
    }
2864

2865
    /**
2866
     * Use a custom function to parse the response.
2867
     *
2868
     * @param callable $callback Takes the raw body of
2869
     *                           the http response and returns a mixed
2870
     *
2871
     * @return static
2872
     */
2873
    public function withParseCallback(callable $callback): self
2874
    {
2875
        $new = clone $this;
3✔
2876

2877
        $new->parse_callback = $callback;
3✔
2878

2879
        return $new;
3✔
2880
    }
2881

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

2905
        $new = $new->withCurlOption(\CURLOPT_PROXY, "{$proxy_host}:{$proxy_port}");
7✔
2906
        $new = $new->withCurlOption(\CURLOPT_PROXYTYPE, $proxy_type);
7✔
2907

2908
        if (\in_array($auth_type, [\CURLAUTH_BASIC, \CURLAUTH_NTLM], true)) {
7✔
2909
            $new = $new->withCurlOption(\CURLOPT_PROXYAUTH, $auth_type);
1✔
2910
            $new = $new->withCurlOption(\CURLOPT_PROXYUSERPWD, "{$auth_username}:{$auth_password}");
1✔
2911
        }
2912

2913
        return $new;
7✔
2914
    }
2915

2916
    /**
2917
     * @return static
2918
     */
2919
    public function withProxyTunnel(bool $tunnel = true): self
2920
    {
2921
        return $this->_withNamedCurlOption('CURLOPT_HTTPPROXYTUNNEL', $tunnel);
1✔
2922
    }
2923

2924
    /**
2925
     * @param string[]|string $hosts
2926
     *
2927
     * @return static
2928
     */
2929
    public function withNoProxy($hosts): self
2930
    {
2931
        if (\is_array($hosts)) {
1✔
2932
            $hosts = \implode(',', $hosts);
1✔
2933
        }
2934

2935
        return $this->_withNamedCurlOption('CURLOPT_NOPROXY', (string) $hosts);
1✔
2936
    }
2937

2938
    /**
2939
     * @param string[]|string $entries
2940
     *
2941
     * @return static
2942
     */
2943
    public function withResolve($entries): self
2944
    {
2945
        return $this->_withNamedCurlOption(
3✔
2946
            'CURLOPT_RESOLVE',
3✔
2947
            $this->_normalizeCurlStringList($entries, 'resolve entries')
3✔
2948
        );
3✔
2949
    }
2950

2951
    /**
2952
     * @param string[]|string $entries
2953
     *
2954
     * @return static
2955
     */
2956
    public function withConnectTo($entries): self
2957
    {
2958
        return $this->_withNamedCurlOption(
2✔
2959
            'CURLOPT_CONNECT_TO',
2✔
2960
            $this->_normalizeCurlStringList($entries, 'connect-to entries')
2✔
2961
        );
2✔
2962
    }
2963

2964
    /**
2965
     * @param string|null $key
2966
     * @param mixed|null  $fallback
2967
     *
2968
     * @return mixed
2969
     */
2970
    public function getHelperData($key = null, $fallback = null)
2971
    {
2972
        if ($key !== null) {
2✔
2973
            return $this->helperData[$key] ?? $fallback;
2✔
2974
        }
2975

2976
        return $this->helperData;
1✔
2977
    }
2978

2979
    /**
2980
     * @return void
2981
     */
2982
    public function clearHelperData()
2983
    {
2984
        $this->helperData = [];
8✔
2985
    }
2986

2987
    /**
2988
     * @param string $key
2989
     * @param mixed  $value
2990
     *
2991
     * @return static
2992
     */
2993
    public function addHelperData(string $key, $value): self
2994
    {
2995
        $this->helperData[$key] = $value;
2✔
2996

2997
        return $this;
2✔
2998
    }
2999

3000
    /**
3001
     * @param callable|null $send_callback
3002
     *
3003
     * @return static
3004
     */
3005
    public function withSendCallback($send_callback): self
3006
    {
3007
        $new = clone $this;
2✔
3008

3009
        if (!empty($send_callback)) {
2✔
3010
            $new->send_callbacks[] = $send_callback;
1✔
3011
        }
3012

3013
        return $new;
2✔
3014
    }
3015

3016
    /**
3017
     * @param callable $callback
3018
     *
3019
     * @return static
3020
     */
3021
    public function withSerializePayload(callable $callback): self
3022
    {
3023
        return $this->registerPayloadSerializer('*', $callback);
1✔
3024
    }
3025

3026
    /**
3027
     * @param string $file_path
3028
     *
3029
     * @return Request
3030
     */
3031
    public function withDownload($file_path): self
3032
    {
3033
        $new = clone $this;
4✔
3034

3035
        $new->file_path_for_download = $file_path;
4✔
3036

3037
        return $new;
4✔
3038
    }
3039

3040
    /**
3041
     * @param string $uri
3042
     * @param bool   $useClone
3043
     *
3044
     * @return static
3045
     */
3046
    public function withUriFromString(string $uri, bool $useClone = true): self
3047
    {
3048
        if ($useClone) {
334✔
3049
            return (clone $this)->withUri(new Uri($uri));
334✔
3050
        }
3051

3052
        return $this->_withUri(new Uri($uri));
2✔
3053
    }
3054

3055
    /**
3056
     * Sets user agent.
3057
     *
3058
     * @param string $userAgent
3059
     *
3060
     * @return static
3061
     */
3062
    public function withUserAgent($userAgent): self
3063
    {
3064
        return $this->withHeader('User-Agent', $userAgent);
2✔
3065
    }
3066

3067
    /**
3068
     * Takes a curl result and generates a Response from it.
3069
     *
3070
     * @param false|mixed $result
3071
     * @param Curl|null   $curl
3072
     *
3073
     * @throws NetworkErrorException
3074
     *
3075
     * @return Response
3076
     *
3077
     * @internal
3078
     */
3079
    public function _buildResponse($result, ?Curl $curl = null): Response
3080
    {
3081
        // fallback
3082
        if ($curl === null) {
37✔
3083
            $curl = $this->curl;
31✔
3084
        }
3085

3086
        if ($curl === null) {
37✔
3087
            throw new NetworkErrorException('Unable to build the response for "' . $this->uri . '". => "curl" === null');
×
3088
        }
3089

3090
        if ($result === false) {
37✔
3091
            $curlErrorNumber = $curl->getErrorCode();
7✔
3092
            if ($curlErrorNumber) {
7✔
3093
                $curlErrorString = (string) $curl->getErrorMessage();
7✔
3094

3095
                $this->_error($curlErrorString);
7✔
3096

3097
                $exception = new NetworkErrorException(
7✔
3098
                    'Unable to connect to "' . $this->uri . '": ' . $curlErrorNumber . ' ' . $curlErrorString,
7✔
3099
                    $curlErrorNumber,
7✔
3100
                    null,
7✔
3101
                    $curl,
7✔
3102
                    $this
7✔
3103
                );
7✔
3104

3105
                $exception->setCurlErrorNumber($curlErrorNumber)->setCurlErrorString($curlErrorString);
7✔
3106

3107
                throw $exception;
7✔
3108
            }
3109

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

3112
            throw new NetworkErrorException('Unable to connect to "' . $this->uri . '".');
×
3113
        }
3114

3115
        $curl_info = $curl->getInfo();
30✔
3116

3117
        $headers = $curl->getRawResponseHeaders();
30✔
3118
        $rawResponse = $curl->getRawResponse();
30✔
3119

3120
        if ($rawResponse === false) {
30✔
3121
            $body = '';
×
3122
        } elseif ($rawResponse === true && $this->file_path_for_download && \is_string($this->file_path_for_download)) {
30✔
3123
            $body = \file_get_contents($this->file_path_for_download);
1✔
3124
            if ($body === false) {
1✔
3125
                throw new \ErrorException('file_get_contents return false for: ' . $this->file_path_for_download);
×
3126
            }
3127
        } else {
3128
            $body = UTF8::remove_left(
29✔
3129
                (string) $rawResponse,
29✔
3130
                $headers
29✔
3131
            );
29✔
3132
        }
3133

3134
        // get the protocol + version
3135
        $protocol_version_regex = "/HTTP\/(?<version>[\d\.]*+)/i";
30✔
3136
        $protocol_version_matches = [];
30✔
3137
        $protocol_version = null;
30✔
3138
        \preg_match($protocol_version_regex, $headers, $protocol_version_matches);
30✔
3139
        if (isset($protocol_version_matches['version'])) {
30✔
3140
            $protocol_version = $protocol_version_matches['version'];
29✔
3141
        }
3142
        $curl_info['protocol_version'] = $protocol_version;
30✔
3143

3144
        // DEBUG
3145
        //var_dump($body, $headers);
3146

3147
        return new Response(
30✔
3148
            $body,
30✔
3149
            $headers,
30✔
3150
            $this,
30✔
3151
            $curl_info
30✔
3152
        );
30✔
3153
    }
3154

3155
    /**
3156
     * @return void
3157
     */
3158
    private function _configureRetryBehavior(): void
3159
    {
3160
        $curl = $this->curl;
101✔
3161
        if ($curl === null) {
101✔
NEW
3162
            throw new \LogicException('cURL is not initialized.');
×
3163
        }
3164

3165
        $curl->attempts = 0;
101✔
3166
        $curl->retries = 0;
101✔
3167

3168
        if ($this->retry <= 0) {
101✔
3169
            $curl->setRetry(0);
93✔
3170

3171
            return;
93✔
3172
        }
3173

3174
        $curl->setRetry($this->_createRetryDecider());
8✔
3175
    }
3176

3177
    /**
3178
     * @return callable
3179
     */
3180
    private function _createRetryDecider(): callable
3181
    {
3182
        $maximum_number_of_retries = $this->retry;
8✔
3183
        $retry_delay = $this->retry_delay;
8✔
3184
        $retry_max_time = $this->retry_max_time;
8✔
3185
        $retry_all_errors = $this->retry_all_errors;
8✔
3186
        $retry_connection_refused = $this->retry_connection_refused;
8✔
3187
        $started_at = null;
8✔
3188
        $retry_attempts = 0;
8✔
3189

3190
        return static function (Curl $curl) use (
8✔
3191
            $maximum_number_of_retries,
8✔
3192
            $retry_delay,
8✔
3193
            $retry_max_time,
8✔
3194
            $retry_all_errors,
8✔
3195
            $retry_connection_refused,
8✔
3196
            &$started_at,
8✔
3197
            &$retry_attempts
8✔
3198
        ): bool {
8✔
3199
            if ($retry_attempts >= $maximum_number_of_retries) {
8✔
3200
                return false;
1✔
3201
            }
3202

3203
            if (
3204
                !$retry_all_errors
8✔
3205
                &&
3206
                !self::_isRetryableError($curl, $retry_connection_refused)
8✔
3207
            ) {
3208
                return false;
1✔
3209
            }
3210

3211
            $delay_in_seconds = $retry_delay;
7✔
3212
            if ($delay_in_seconds === null) {
7✔
3213
                $delay_in_seconds = \min(600.0, (float) (2 ** $retry_attempts));
1✔
3214
            }
3215

3216
            if ($started_at === null) {
7✔
3217
                $started_at = \microtime(true);
7✔
3218
            }
3219

3220
            if (
3221
                $retry_max_time !== null
7✔
3222
                &&
3223
                ((\microtime(true) - $started_at) + $delay_in_seconds) > $retry_max_time
7✔
3224
            ) {
3225
                return false;
1✔
3226
            }
3227

3228
            if ($delay_in_seconds > 0) {
6✔
3229
                \usleep((int) \round($delay_in_seconds * 1000000));
2✔
3230
            }
3231

3232
            ++$retry_attempts;
6✔
3233

3234
            return true;
6✔
3235
        };
8✔
3236
    }
3237

3238
    /**
3239
     * @return bool
3240
     */
3241
    private static function _isRetryableError(Curl $curl, bool $retry_connection_refused): bool
3242
    {
3243
        if ($curl->isCurlError()) {
8✔
3244
            if ($curl->getCurlErrorCode() === \CURLE_OPERATION_TIMEOUTED) {
3✔
3245
                return true;
1✔
3246
            }
3247

3248
            if (
3249
                $retry_connection_refused
2✔
3250
                &&
3251
                $curl->getCurlErrorCode() === \CURLE_COULDNT_CONNECT
2✔
3252
                &&
3253
                \stripos((string) $curl->getCurlErrorMessage(), 'Connection refused') !== false
2✔
3254
            ) {
3255
                return true;
1✔
3256
            }
3257

3258
            return false;
1✔
3259
        }
3260

3261
        return \in_array(
5✔
3262
            $curl->getHttpStatusCode(),
5✔
3263
            [408, 429, 500, 502, 503, 504],
5✔
3264
            true
5✔
3265
        );
5✔
3266
    }
3267

3268
    /**
3269
     * @return int
3270
     */
3271
    private static function _requireCurlConstant(string $constant_name): int
3272
    {
3273
        if (!\defined($constant_name)) {
5✔
NEW
3274
            throw new \RuntimeException('The installed cURL extension does not support ' . $constant_name . '.');
×
3275
        }
3276

3277
        return (int) \constant($constant_name);
5✔
3278
    }
3279

3280
    /**
3281
     * @return static
3282
     */
3283
    private function _withCurlHttpVersion(string $protocol_version, string $curl_http_version_constant_name): self
3284
    {
3285
        $new = clone $this;
1✔
3286
        $new->protocol_version = $protocol_version;
1✔
3287
        $new->curl_http_version = $curl_http_version_constant_name;
1✔
3288

3289
        return $new;
1✔
3290
    }
3291

3292
    /**
3293
     * @return int
3294
     */
3295
    private function _resolveCurlHttpVersion(): int
3296
    {
3297
        if (\is_int($this->curl_http_version)) {
101✔
3298
            return $this->curl_http_version;
2✔
3299
        }
3300

3301
        if (\is_string($this->curl_http_version) && $this->curl_http_version !== '') {
99✔
NEW
3302
            return self::_requireCurlConstant($this->curl_http_version);
×
3303
        }
3304

3305
        switch ((string) $this->protocol_version) {
99✔
3306
            case Http::HTTP_1_0:
3307
                return \CURL_HTTP_VERSION_1_0;
1✔
3308
            case Http::HTTP_1_1:
99✔
3309
                return \CURL_HTTP_VERSION_1_1;
97✔
3310
            case Http::HTTP_2_0:
2✔
3311
                return \CURL_HTTP_VERSION_2_0;
1✔
3312
            case Http::HTTP_3:
2✔
NEW
3313
                return self::_requireCurlConstant('CURL_HTTP_VERSION_3');
×
3314
            default:
3315
                return \CURL_HTTP_VERSION_NONE;
2✔
3316
        }
3317
    }
3318

3319
    /**
3320
     * @param mixed $value
3321
     *
3322
     * @return static
3323
     */
3324
    private function _withNamedCurlOption(string $curl_option_constant_name, $value): self
3325
    {
3326
        $new = clone $this;
4✔
3327
        $new->_withCurlOptionValue(self::_requireCurlConstant($curl_option_constant_name), $value);
4✔
3328

3329
        return $new;
4✔
3330
    }
3331

3332
    /**
3333
     * @param mixed $curl_opt_val
3334
     *
3335
     * @return static
3336
     */
3337
    private function _withCurlOptionValue(int $curl_opt, $curl_opt_val): self
3338
    {
3339
        $this->additional_curl_opts[$curl_opt] = $curl_opt_val;
19✔
3340

3341
        return $this;
19✔
3342
    }
3343

3344
    /**
3345
     * @param array<int, mixed>|string $entries
3346
     *
3347
     * @return string[]
3348
     */
3349
    private function _normalizeCurlStringList($entries, string $description): array
3350
    {
3351
        if (!\is_array($entries)) {
3✔
3352
            $entries = [$entries];
1✔
3353
        }
3354

3355
        $normalized_entries = [];
3✔
3356
        foreach ($entries as $entry) {
3✔
3357
            if (!\is_string($entry) || $entry === '') {
3✔
3358
                throw new \InvalidArgumentException('Invalid ' . $description . ' provided: ' . \var_export($entry, true));
1✔
3359
            }
3360

3361
            $normalized_entries[] = $entry;
2✔
3362
        }
3363

3364
        return $normalized_entries;
2✔
3365
    }
3366

3367
    /**
3368
     * @param string|int $version
3369
     *
3370
     * @return array{0:int,1:int|null}
3371
     */
3372
    private function _normalizeTlsVersionOption($version, bool $maximum): array
3373
    {
3374
        if (\is_int($version)) {
4✔
3375
            return [$version, null];
1✔
3376
        }
3377

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

3380
        if ($normalized_version === 'default') {
4✔
3381
            return [self::_requireCurlConstant('CURL_SSLVERSION_DEFAULT'), 0];
1✔
3382
        }
3383

3384
        $map = [
3✔
3385
            '1'   => ['CURL_SSLVERSION_TLSv1', 1],
3✔
3386
            '1.0' => [$maximum ? 'CURL_SSLVERSION_MAX_TLSv1_0' : 'CURL_SSLVERSION_TLSv1_0', 1],
3✔
3387
            '1.1' => [$maximum ? 'CURL_SSLVERSION_MAX_TLSv1_1' : 'CURL_SSLVERSION_TLSv1_1', 2],
3✔
3388
            '1.2' => [$maximum ? 'CURL_SSLVERSION_MAX_TLSv1_2' : 'CURL_SSLVERSION_TLSv1_2', 3],
3✔
3389
            '1.3' => [$maximum ? 'CURL_SSLVERSION_MAX_TLSv1_3' : 'CURL_SSLVERSION_TLSv1_3', 4],
3✔
3390
        ];
3✔
3391

3392
        if (!isset($map[$normalized_version])) {
3✔
3393
            throw new \InvalidArgumentException('Invalid TLS version provided: ' . \var_export($version, true));
1✔
3394
        }
3395

3396
        return [self::_requireCurlConstant($map[$normalized_version][0]), $map[$normalized_version][1]];
2✔
3397
    }
3398

3399
    /**
3400
     * @return int
3401
     */
3402
    private function _buildAltSvcControlValue(bool $read_only): int
3403
    {
3404
        $control = 0;
1✔
3405

3406
        foreach (['CURLALTSVC_H1', 'CURLALTSVC_H2', 'CURLALTSVC_H3'] as $constant_name) {
1✔
3407
            if (\defined($constant_name)) {
1✔
3408
                $control |= (int) \constant($constant_name);
1✔
3409
            }
3410
        }
3411

3412
        if ($control === 0) {
1✔
NEW
3413
            throw new \RuntimeException('The installed cURL extension does not support Alt-Svc control flags.');
×
3414
        }
3415

3416
        if ($read_only) {
1✔
3417
            $control |= self::_requireCurlConstant('CURLALTSVC_READONLYFILE');
1✔
3418
        }
3419

3420
        return $control;
1✔
3421
    }
3422

3423
    /**
3424
     * @return int
3425
     */
3426
    private function _buildHstsControlValue(bool $read_only): int
3427
    {
3428
        $control = self::_requireCurlConstant('CURLHSTS_ENABLE');
1✔
3429

3430
        if ($read_only) {
1✔
3431
            $control |= self::_requireCurlConstant('CURLHSTS_READONLYFILE');
1✔
3432
        }
3433

3434
        return $control;
1✔
3435
    }
3436

3437
    /**
3438
     * @param float|int $value
3439
     *
3440
     * @return float|int
3441
     */
3442
    private function _normalizeDurationValue($value, string $description)
3443
    {
3444
        if (!\preg_match('/^\d+(\.\d+)?$/', (string) $value)) {
10✔
3445
            throw new \InvalidArgumentException(
2✔
3446
                'Invalid ' . $description . ' provided: ' . \var_export($value, true)
2✔
3447
            );
2✔
3448
        }
3449

3450
        return $value + 0;
8✔
3451
    }
3452

3453
    /**
3454
     * @param bool $auto_parse perform automatic "smart"
3455
     *                         parsing based on Content-Type or "expectedType"
3456
     *                         If not auto parsing, Response->body returns the body
3457
     *                         as a string
3458
     *
3459
     * @return static
3460
     */
3461
    private function _autoParse(bool $auto_parse = true): self
3462
    {
3463
        $new = clone $this;
7✔
3464

3465
        $new->auto_parse = $auto_parse;
7✔
3466

3467
        return $new;
7✔
3468
    }
3469

3470
    /**
3471
     * @param mixed $str payload
3472
     *
3473
     * @return int length of payload in bytes
3474
     */
3475
    private function _determineLength($str): int
3476
    {
3477
        if ($str === null || \is_array($str)) {
18✔
3478
            return 0;
1✔
3479
        }
3480

3481
        return \strlen((string) $str);
17✔
3482
    }
3483

3484
    /**
3485
     * @param string $error
3486
     *
3487
     * @return void
3488
     */
3489
    private function _error($error)
3490
    {
3491
        // global error handling
3492

3493
        $global_error_handler = Setup::getGlobalErrorHandler();
7✔
3494
        if ($global_error_handler) {
7✔
3495
            if ($global_error_handler instanceof LoggerInterface) {
×
3496
                // PSR-3 https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
3497
                $global_error_handler->error($error);
×
3498
            } elseif (\is_callable($global_error_handler)) {
×
3499
                // error callback
3500
                /** @noinspection VariableFunctionsUsageInspection */
3501
                \call_user_func($global_error_handler, $error);
×
3502
            }
3503
        }
3504

3505
        // local error handling
3506

3507
        if (isset($this->error_handler)) {
7✔
3508
            if ($this->error_handler instanceof LoggerInterface) {
3✔
3509
                // PSR-3 https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
3510
                $this->error_handler->error($error);
×
3511
            } elseif (\is_callable($this->error_handler)) {
3✔
3512
                // error callback
3513
                \call_user_func($this->error_handler, $error);
3✔
3514
            }
3515
        } else {
3516
            /** @noinspection ForgottenDebugOutputInspection */
3517
            \error_log($error);
4✔
3518
        }
3519
    }
3520

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

3542
        if ($this->serialize_payload_method === static::SERIALIZE_PAYLOAD_NEVER) {
18✔
3543
            return $payload;
1✔
3544
        }
3545

3546
        // When we are in "smart" mode, don't serialize strings/scalars, assume they are already serialized.
3547
        if (
3548
            $this->serialize_payload_method === static::SERIALIZE_PAYLOAD_SMART
17✔
3549
            &&
3550
            \is_array($payload)
17✔
3551
            &&
3552
            \count($payload) === 1
17✔
3553
            &&
3554
            \array_keys($payload)[0] === 0
17✔
3555
            &&
3556
            \is_scalar($payload_first = \array_values($payload)[0])
17✔
3557
        ) {
3558
            return $payload_first;
5✔
3559
        }
3560

3561
        // Use a custom serializer if one is registered for this mime type.
3562
        $issetContentType = isset($this->payload_serializers[$this->content_type]);
12✔
3563
        if (
3564
            $issetContentType
12✔
3565
            ||
3566
            isset($this->payload_serializers['*'])
12✔
3567
        ) {
3568
            if ($issetContentType) {
×
3569
                $key = $this->content_type;
×
3570
            } else {
3571
                $key = '*';
×
3572
            }
3573

3574
            return \call_user_func($this->payload_serializers[$key], $payload);
×
3575
        }
3576

3577
        return Setup::setupGlobalMimeType($this->content_type)->serialize($payload);
12✔
3578
    }
3579

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

3594
        if (!empty($payload)) {
44✔
3595
            if (\is_array($payload)) {
33✔
3596
                foreach ($payload as $keyInner => $valueInner) {
17✔
3597
                    $this->_setBody($valueInner, $keyInner, $mimeType);
17✔
3598
                }
3599

3600
                return $this;
17✔
3601
            }
3602

3603
            if ($payload instanceof StreamInterface) {
33✔
3604
                $this->payload = (string) $payload;
3✔
3605
            } elseif ($key === null) {
30✔
3606
                if (\is_string($this->payload)) {
13✔
3607
                    $tmpPayload = $this->payload;
×
3608
                    $this->payload = [];
×
3609
                    $this->payload[] = $tmpPayload;
×
3610
                }
3611

3612
                $this->payload[] = $payload;
13✔
3613
            } else {
3614
                if (\is_string($this->payload)) {
17✔
3615
                    $tmpPayload = $this->payload;
×
3616
                    $this->payload = [];
×
3617
                    $this->payload[] = $tmpPayload;
×
3618
                }
3619

3620
                $this->payload[$key] = $payload;
17✔
3621
            }
3622
        }
3623

3624
        // Don't call _serializePayload yet.
3625
        // Wait until we actually send off the request to convert payload to string.
3626
        // At that time, the `serialized_payload` is set accordingly.
3627

3628
        return $this;
44✔
3629
    }
3630

3631
    /**
3632
     * Set the defaults on a newly instantiated object
3633
     * Doesn't copy variables prefixed with _
3634
     *
3635
     * @return static
3636
     */
3637
    private function _setDefaultsFromTemplate(): self
3638
    {
3639
        if ($this->template !== null) {
381✔
3640
            if (\function_exists('gzdecode')) {
381✔
3641
                $this->template->content_encoding = 'gzip';
381✔
3642
            } elseif (\function_exists('gzinflate')) {
×
3643
                $this->template->content_encoding = 'deflate';
×
3644
            }
3645

3646
            foreach ($this->template as $k => $v) {
381✔
3647
                if ($k[0] !== '_') {
381✔
3648
                    $this->{$k} = $v;
381✔
3649
                }
3650
            }
3651
        }
3652

3653
        return $this;
381✔
3654
    }
3655

3656
    /**
3657
     * Set the method.  Shouldn't be called often as the preferred syntax
3658
     * for instantiation is the method specific factory methods.
3659
     *
3660
     * @param string|null $method
3661
     *
3662
     * @return static
3663
     */
3664
    private function _setMethod($method): self
3665
    {
3666
        if (empty($method)) {
381✔
3667
            return $this;
51✔
3668
        }
3669

3670
        if (!\in_array($method, Http::allMethods(), true)) {
381✔
3671
            throw new RequestException($this, 'Unknown HTTP method: \'' . \strip_tags($method) . '\'');
1✔
3672
        }
3673

3674
        $this->method = $method;
381✔
3675

3676
        return $this;
381✔
3677
    }
3678

3679
    /**
3680
     * Do we strictly enforce SSL verification?
3681
     *
3682
     * @param bool $strict
3683
     *
3684
     * @return static
3685
     */
3686
    private function _strictSSL($strict): self
3687
    {
3688
        $new = clone $this;
381✔
3689

3690
        $new->strict_ssl = $strict;
381✔
3691

3692
        return $new;
381✔
3693
    }
3694

3695
    /**
3696
     * @return void
3697
     */
3698
    private function _updateHostFromUri()
3699
    {
3700
        if ($this->uri === null) {
335✔
3701
            return;
×
3702
        }
3703

3704
        if ($this->uri_cache === \serialize($this->uri)) {
335✔
3705
            return;
3✔
3706
        }
3707

3708
        $host = $this->uri->getHost();
335✔
3709

3710
        if ($host === '') {
335✔
3711
            return;
15✔
3712
        }
3713

3714
        $port = $this->uri->getPort();
322✔
3715
        if ($port !== null) {
322✔
3716
            $host .= ':' . $port;
68✔
3717
        }
3718

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

3723
        $this->uri_cache = \serialize($this->uri);
322✔
3724
    }
3725

3726
    /**
3727
     * @param string|null $mime     use a constant from Mime::*
3728
     * @param string|null $fallback use a constant from Mime::*
3729
     *
3730
     * @return static
3731
     */
3732
    private function _withContentType($mime, ?string $fallback = null): self
3733
    {
3734
        if (empty($mime) && empty($fallback)) {
381✔
3735
            return $this;
×
3736
        }
3737

3738
        if (empty($mime)) {
381✔
3739
            $mime = $fallback;
381✔
3740
        }
3741

3742
        $this->content_type = Mime::getFullMime($mime);
381✔
3743

3744
        if ($this->isUpload()) {
381✔
3745
            $this->neverSerializePayload();
5✔
3746
        }
3747

3748
        return $this;
381✔
3749
    }
3750

3751
    /**
3752
     * @param string|null $mime     use a constant from Mime::*
3753
     * @param string|null $fallback use a constant from Mime::*
3754
     *
3755
     * @return static
3756
     */
3757
    private function _withExpectedType($mime, ?string $fallback = null): self
3758
    {
3759
        if (empty($mime) && empty($fallback)) {
381✔
3760
            return $this;
×
3761
        }
3762

3763
        if (empty($mime)) {
381✔
3764
            $mime = $fallback;
381✔
3765
        }
3766

3767
        $this->expected_type = Mime::getFullMime($mime);
381✔
3768

3769
        return $this;
381✔
3770
    }
3771

3772
    /**
3773
     * Helper function to set the Content type and Expected as same in one swoop.
3774
     *
3775
     * @param string|null $mime mime type to use for content type and expected return type
3776
     *
3777
     * @return static
3778
     */
3779
    private function _withMimeType($mime): self
3780
    {
3781
        if (empty($mime)) {
303✔
3782
            return $this;
248✔
3783
        }
3784

3785
        $this->expected_type = Mime::getFullMime($mime);
59✔
3786
        $this->content_type = $this->expected_type;
59✔
3787

3788
        if ($this->isUpload()) {
59✔
3789
            $this->neverSerializePayload();
1✔
3790
        }
3791

3792
        return $this;
59✔
3793
    }
3794

3795
    /**
3796
     * @param UriInterface $uri
3797
     * @param bool         $preserveHost
3798
     *
3799
     * @return static
3800
     */
3801
    private function _withUri(UriInterface $uri, $preserveHost = false): self
3802
    {
3803
        if ($this->uri === $uri) {
335✔
3804
            return $this;
2✔
3805
        }
3806

3807
        $this->uri = $uri;
335✔
3808

3809
        if (!$preserveHost) {
335✔
3810
            $this->_updateHostFromUri();
335✔
3811
        }
3812

3813
        return $this;
335✔
3814
    }
3815
}
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