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

voku / httpful / 25187302842

30 Apr 2026 08:18PM UTC coverage: 90.48% (+0.6%) from 89.902%
25187302842

Pull #26

github

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

366 of 403 new or added lines in 7 files covered. (90.82%)

47 existing lines in 1 file now uncovered.

2547 of 2815 relevant lines covered (90.48%)

49.02 hits per line

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

87.64
/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
        if ($name === '') {
10✔
1221
            throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.');
1✔
1222
        }
1223

1224
        $new = clone $this;
9✔
1225

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

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

1241
        return $new;
9✔
1242
    }
1243

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

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

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

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

1291
        $new->headers->forceSet($name, $value);
25✔
1292

1293
        return $new;
24✔
1294
    }
1295

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

1318
        $new->_setMethod($method);
3✔
1319

1320
        return $new;
3✔
1321
    }
1322

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

1342
        $new->protocol_version = $version;
6✔
1343
        $new->curl_http_version = null;
6✔
1344

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

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

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

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

1361
                break;
1✔
1362
        }
1363

1364
        return $new;
6✔
1365
    }
1366

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

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

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

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

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

1424
        $new = clone $this;
3✔
1425

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

1430
        return $new;
3✔
1431
    }
1432

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

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

1487
        $new->headers->forceUnset($name);
322✔
1488

1489
        return $new;
322✔
1490
    }
1491

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

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

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

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

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

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

1536
        return $elements;
381✔
1537
    }
1538

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

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

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

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

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

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

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

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

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

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

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

UNCOV
1626
        return $this->curlMulti->getMultiCurl() !== null;
×
1627
    }
1628

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1793
        return $request->withUriFromString($uri);
4✔
1794
    }
1795

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

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

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

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

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

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

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

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

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

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

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

1881
        return $new;
2✔
1882
    }
1883

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

1891
        $this->close();
1✔
1892
        $this->initialize();
1✔
1893
    }
1894

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

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

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

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

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

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

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

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

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

1988
        return $this->curlMulti;
27✔
1989
    }
1990

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

2006
        $result = $curl->exec();
31✔
2007

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

2020
                $result = $this->curl->exec();
×
2021

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

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

2034
        if (!$this->keep_alive) {
31✔
UNCOV
2035
            $this->close();
×
2036
        }
2037

2038
        return $this->_buildResponse($result);
31✔
2039
    }
2040

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

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

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

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

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

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

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

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

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

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

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

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

2156
        return $this;
9✔
2157
    }
2158

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

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

2186
        $new = clone $this;
8✔
2187

2188
        $new->timeout = $timeout;
8✔
2189

2190
        return $new;
8✔
2191
    }
2192

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

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

2209
        return $new;
9✔
2210
    }
2211

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

2222
        return $new;
8✔
2223
    }
2224

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

2235
        return $new;
3✔
2236
    }
2237

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

2246
        return $new;
2✔
2247
    }
2248

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

2257
        return $new;
2✔
2258
    }
2259

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

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

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

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

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

NEW
2346
            return $new;
×
2347
        }
2348

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

2359
        \finfo_close($fInfo);
1✔
2360

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

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

2380
        return $new;
13✔
2381
    }
2382

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

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

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

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

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

2432
        $new = clone $this;
6✔
2433

2434
        $new->connection_timeout = $connection_timeout;
6✔
2435

2436
        return $new;
6✔
2437
    }
2438

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

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

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

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

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

2483
            $ssl_version |= $maximum_option;
1✔
2484
        }
2485

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

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

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

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

2513
        $new->cache_control = $cache_control;
5✔
2514

2515
        return $new;
5✔
2516
    }
2517

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

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

2532
        $new->content_charset = UTF8::normalize_encoding($charset);
1✔
2533

2534
        return $new;
1✔
2535
    }
2536

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

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

2552
        return $new;
5✔
2553
    }
2554

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

2564
        $new->content_encoding = $encoding;
6✔
2565

2566
        return $new;
6✔
2567
    }
2568

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

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

2588
        return $new;
1✔
2589
    }
2590

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

2599
        return $new;
2✔
2600
    }
2601

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

2610
        return $new;
1✔
2611
    }
2612

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

2621
        return $new;
1✔
2622
    }
2623

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

2632
        return $new;
1✔
2633
    }
2634

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

2643
        return $new;
1✔
2644
    }
2645

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

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

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

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

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

2698
        $new->_withCurlOptionValue($curl_opt, $curl_opt_val);
15✔
2699

2700
        return $new;
15✔
2701
    }
2702

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

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

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

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

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

2745
        $new = $new->withCurlOption(\CURLOPT_HTTPAUTH, \CURLAUTH_DIGEST);
4✔
2746

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

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

2762
        $new->error_handler = $error_handler;
4✔
2763

2764
        return $new;
4✔
2765
    }
2766

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

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

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

2791
        return $new;
4✔
2792
    }
2793

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

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

2817
        $new = $new->withCurlOption(\CURLOPT_HTTPAUTH, \CURLAUTH_NTLM);
2✔
2818

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

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

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

2842
        return $new;
5✔
2843
    }
2844

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

2858
        $new->params = \array_merge($new->params, $params);
3✔
2859

2860
        return $new;
3✔
2861
    }
2862

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

2875
        $new->parse_callback = $callback;
3✔
2876

2877
        return $new;
3✔
2878
    }
2879

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

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

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

2911
        return $new;
7✔
2912
    }
2913

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

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

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

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

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

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

2974
        return $this->helperData;
1✔
2975
    }
2976

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

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

2995
        return $this;
2✔
2996
    }
2997

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

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

3011
        return $new;
2✔
3012
    }
3013

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

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

3033
        $new->file_path_for_download = $file_path;
4✔
3034

3035
        return $new;
4✔
3036
    }
3037

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

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

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

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

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

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

3093
                $this->_error($curlErrorString);
7✔
3094

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

3103
                $exception->setCurlErrorNumber($curlErrorNumber)->setCurlErrorString($curlErrorString);
7✔
3104

3105
                throw $exception;
7✔
3106
            }
3107

UNCOV
3108
            $this->_error('Unable to connect to "' . $this->uri . '".');
×
3109

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

3113
        $curl_info = $curl->getInfo();
30✔
3114

3115
        $headers = $curl->getRawResponseHeaders();
30✔
3116
        $rawResponse = $curl->getRawResponse();
30✔
3117

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

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

3142
        // DEBUG
3143
        //var_dump($body, $headers);
3144

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

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

3163
        $curl->attempts = 0;
101✔
3164
        $curl->retries = 0;
101✔
3165

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

3169
            return;
93✔
3170
        }
3171

3172
        $curl->setRetry($this->_createRetryDecider());
8✔
3173
    }
3174

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

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

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

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

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

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

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

3230
            ++$retry_attempts;
6✔
3231

3232
            return true;
6✔
3233
        };
8✔
3234
    }
3235

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

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

3256
            return false;
1✔
3257
        }
3258

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

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

3275
        return (int) \constant($constant_name);
5✔
3276
    }
3277

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

3287
        return $new;
1✔
3288
    }
3289

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

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

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

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

3327
        return $new;
4✔
3328
    }
3329

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

3339
        return $this;
19✔
3340
    }
3341

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

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

3359
            $normalized_entries[] = $entry;
2✔
3360
        }
3361

3362
        return $normalized_entries;
2✔
3363
    }
3364

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

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

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

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

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

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

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

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

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

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

3418
        return $control;
1✔
3419
    }
3420

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

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

3432
        return $control;
1✔
3433
    }
3434

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

3448
        return $value + 0;
8✔
3449
    }
3450

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

3463
        $new->auto_parse = $auto_parse;
7✔
3464

3465
        return $new;
7✔
3466
    }
3467

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

3479
        return \strlen((string) $str);
17✔
3480
    }
3481

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

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

3503
        // local error handling
3504

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

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

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

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

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

UNCOV
3572
            return \call_user_func($this->payload_serializers[$key], $payload);
×
3573
        }
3574

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

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

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

3598
                return $this;
17✔
3599
            }
3600

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

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

3618
                $this->payload[$key] = $payload;
17✔
3619
            }
3620
        }
3621

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

3626
        return $this;
44✔
3627
    }
3628

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

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

3651
        return $this;
381✔
3652
    }
3653

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

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

3672
        $this->method = $method;
381✔
3673

3674
        return $this;
381✔
3675
    }
3676

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

3688
        $new->strict_ssl = $strict;
381✔
3689

3690
        return $new;
381✔
3691
    }
3692

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

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

3706
        $host = $this->uri->getHost();
335✔
3707

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

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

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

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

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

3736
        if (empty($mime)) {
381✔
3737
            $mime = $fallback;
381✔
3738
        }
3739

3740
        $this->content_type = Mime::getFullMime($mime);
381✔
3741

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

3746
        return $this;
381✔
3747
    }
3748

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

3761
        if (empty($mime)) {
381✔
3762
            $mime = $fallback;
381✔
3763
        }
3764

3765
        $this->expected_type = Mime::getFullMime($mime);
381✔
3766

3767
        return $this;
381✔
3768
    }
3769

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

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

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

3790
        return $this;
59✔
3791
    }
3792

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

3805
        $this->uri = $uri;
335✔
3806

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

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