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

codeigniter4 / CodeIgniter4 / 12673986434

08 Jan 2025 03:42PM UTC coverage: 84.455% (+0.001%) from 84.454%
12673986434

Pull #9385

github

web-flow
Merge 06e47f0ee into e475fd8fa
Pull Request #9385: refactor: Fix phpstan expr.resultUnused

20699 of 24509 relevant lines covered (84.45%)

190.57 hits per line

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

98.68
/system/HTTP/CURLRequest.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\HTTP;
15

16
use CodeIgniter\Exceptions\InvalidArgumentException;
17
use CodeIgniter\HTTP\Exceptions\HTTPException;
18
use Config\App;
19
use Config\CURLRequest as ConfigCURLRequest;
20

21
/**
22
 * A lightweight HTTP client for sending synchronous HTTP requests via cURL.
23
 *
24
 * @see \CodeIgniter\HTTP\CURLRequestTest
25
 */
26
class CURLRequest extends OutgoingRequest
27
{
28
    /**
29
     * The response object associated with this request
30
     *
31
     * @var ResponseInterface|null
32
     */
33
    protected $response;
34

35
    /**
36
     * The original response object associated with this request
37
     *
38
     * @var ResponseInterface|null
39
     */
40
    protected $responseOrig;
41

42
    /**
43
     * The URI associated with this request
44
     *
45
     * @var URI
46
     */
47
    protected $baseURI;
48

49
    /**
50
     * The setting values
51
     *
52
     * @var array
53
     */
54
    protected $config;
55

56
    /**
57
     * The default setting values
58
     *
59
     * @var array
60
     */
61
    protected $defaultConfig = [
62
        'timeout'         => 0.0,
63
        'connect_timeout' => 150,
64
        'debug'           => false,
65
        'verify'          => true,
66
    ];
67

68
    /**
69
     * Default values for when 'allow_redirects'
70
     * option is true.
71
     *
72
     * @var array
73
     */
74
    protected $redirectDefaults = [
75
        'max'       => 5,
76
        'strict'    => true,
77
        'protocols' => [
78
            'http',
79
            'https',
80
        ],
81
    ];
82

83
    /**
84
     * The number of milliseconds to delay before
85
     * sending the request.
86
     *
87
     * @var float
88
     */
89
    protected $delay = 0.0;
90

91
    /**
92
     * The default options from the constructor. Applied to all requests.
93
     */
94
    private readonly array $defaultOptions;
95

96
    /**
97
     * Whether share options between requests or not.
98
     *
99
     * If true, all the options won't be reset between requests.
100
     * It may cause an error request with unnecessary headers.
101
     */
102
    private readonly bool $shareOptions;
103

104
    /**
105
     * Takes an array of options to set the following possible class properties:
106
     *
107
     *  - baseURI
108
     *  - timeout
109
     *  - any other request options to use as defaults.
110
     *
111
     * @param array<string, mixed> $options
112
     */
113
    public function __construct(App $config, URI $uri, ?ResponseInterface $response = null, array $options = [])
114
    {
115
        if (! function_exists('curl_version')) {
150✔
116
            throw HTTPException::forMissingCurl(); // @codeCoverageIgnore
×
117
        }
118

119
        parent::__construct(Method::GET, $uri);
150✔
120

121
        $this->responseOrig = $response ?? new Response($config);
150✔
122
        // Remove the default Content-Type header.
123
        $this->responseOrig->removeHeader('Content-Type');
150✔
124

125
        $this->baseURI        = $uri->useRawQueryString();
150✔
126
        $this->defaultOptions = $options;
150✔
127

128
        /** @var ConfigCURLRequest|null $configCURLRequest */
129
        $configCURLRequest  = config(ConfigCURLRequest::class);
150✔
130
        $this->shareOptions = $configCURLRequest->shareOptions ?? true;
150✔
131

132
        $this->config = $this->defaultConfig;
150✔
133
        $this->parseOptions($options);
150✔
134
    }
135

136
    /**
137
     * Sends an HTTP request to the specified $url. If this is a relative
138
     * URL, it will be merged with $this->baseURI to form a complete URL.
139
     *
140
     * @param string $method HTTP method
141
     */
142
    public function request($method, string $url, array $options = []): ResponseInterface
143
    {
144
        $this->response = clone $this->responseOrig;
136✔
145

146
        $this->parseOptions($options);
136✔
147

148
        $url = $this->prepareURL($url);
136✔
149

150
        $method = esc(strip_tags($method));
136✔
151

152
        $this->send($method, $url);
136✔
153

154
        if ($this->shareOptions === false) {
132✔
155
            $this->resetOptions();
67✔
156
        }
157

158
        return $this->response;
132✔
159
    }
160

161
    /**
162
     * Reset all options to default.
163
     *
164
     * @return void
165
     */
166
    protected function resetOptions()
167
    {
168
        // Reset headers
169
        $this->headers   = [];
67✔
170
        $this->headerMap = [];
67✔
171

172
        // Reset body
173
        $this->body = null;
67✔
174

175
        // Reset configs
176
        $this->config = $this->defaultConfig;
67✔
177

178
        // Set the default options for next request
179
        $this->parseOptions($this->defaultOptions);
67✔
180
    }
181

182
    /**
183
     * Convenience method for sending a GET request.
184
     */
185
    public function get(string $url, array $options = []): ResponseInterface
186
    {
187
        return $this->request(Method::GET, $url, $options);
38✔
188
    }
189

190
    /**
191
     * Convenience method for sending a DELETE request.
192
     */
193
    public function delete(string $url, array $options = []): ResponseInterface
194
    {
195
        return $this->request('DELETE', $url, $options);
2✔
196
    }
197

198
    /**
199
     * Convenience method for sending a HEAD request.
200
     */
201
    public function head(string $url, array $options = []): ResponseInterface
202
    {
203
        return $this->request('HEAD', $url, $options);
2✔
204
    }
205

206
    /**
207
     * Convenience method for sending an OPTIONS request.
208
     */
209
    public function options(string $url, array $options = []): ResponseInterface
210
    {
211
        return $this->request('OPTIONS', $url, $options);
2✔
212
    }
213

214
    /**
215
     * Convenience method for sending a PATCH request.
216
     */
217
    public function patch(string $url, array $options = []): ResponseInterface
218
    {
219
        return $this->request('PATCH', $url, $options);
2✔
220
    }
221

222
    /**
223
     * Convenience method for sending a POST request.
224
     */
225
    public function post(string $url, array $options = []): ResponseInterface
226
    {
227
        return $this->request(Method::POST, $url, $options);
16✔
228
    }
229

230
    /**
231
     * Convenience method for sending a PUT request.
232
     */
233
    public function put(string $url, array $options = []): ResponseInterface
234
    {
235
        return $this->request(Method::PUT, $url, $options);
2✔
236
    }
237

238
    /**
239
     * Set the HTTP Authentication.
240
     *
241
     * @param string $type basic or digest
242
     *
243
     * @return $this
244
     */
245
    public function setAuth(string $username, string $password, string $type = 'basic')
246
    {
247
        $this->config['auth'] = [
4✔
248
            $username,
4✔
249
            $password,
4✔
250
            $type,
4✔
251
        ];
4✔
252

253
        return $this;
4✔
254
    }
255

256
    /**
257
     * Set form data to be sent.
258
     *
259
     * @param bool $multipart Set TRUE if you are sending CURLFiles
260
     *
261
     * @return $this
262
     */
263
    public function setForm(array $params, bool $multipart = false)
264
    {
265
        if ($multipart) {
2✔
266
            $this->config['multipart'] = $params;
2✔
267
        } else {
268
            $this->config['form_params'] = $params;
2✔
269
        }
270

271
        return $this;
2✔
272
    }
273

274
    /**
275
     * Set JSON data to be sent.
276
     *
277
     * @param array|bool|float|int|object|string|null $data
278
     *
279
     * @return $this
280
     */
281
    public function setJSON($data)
282
    {
283
        $this->config['json'] = $data;
2✔
284

285
        return $this;
2✔
286
    }
287

288
    /**
289
     * Sets the correct settings based on the options array
290
     * passed in.
291
     *
292
     * @return void
293
     */
294
    protected function parseOptions(array $options)
295
    {
296
        if (array_key_exists('baseURI', $options)) {
151✔
297
            $this->baseURI = $this->baseURI->setURI($options['baseURI']);
44✔
298
            unset($options['baseURI']);
44✔
299
        }
300

301
        if (array_key_exists('headers', $options) && is_array($options['headers'])) {
151✔
302
            foreach ($options['headers'] as $name => $value) {
6✔
303
                $this->setHeader($name, $value);
6✔
304
            }
305

306
            unset($options['headers']);
6✔
307
        }
308

309
        if (array_key_exists('delay', $options)) {
151✔
310
            // Convert from the milliseconds passed in
311
            // to the seconds that sleep requires.
312
            $this->delay = (float) $options['delay'] / 1000;
24✔
313
            unset($options['delay']);
24✔
314
        }
315

316
        if (array_key_exists('body', $options)) {
151✔
317
            $this->setBody($options['body']);
2✔
318
            unset($options['body']);
2✔
319
        }
320

321
        foreach ($options as $key => $value) {
151✔
322
            $this->config[$key] = $value;
72✔
323
        }
324
    }
325

326
    /**
327
     * If the $url is a relative URL, will attempt to create
328
     * a full URL by prepending $this->baseURI to it.
329
     */
330
    protected function prepareURL(string $url): string
331
    {
332
        // If it's a full URI, then we have nothing to do here...
333
        if (str_contains($url, '://')) {
138✔
334
            return $url;
70✔
335
        }
336

337
        $uri = $this->baseURI->resolveRelativeURI($url);
68✔
338

339
        // Create the string instead of casting to prevent baseURL muddling
340
        return URI::createURIString(
68✔
341
            $uri->getScheme(),
68✔
342
            $uri->getAuthority(),
68✔
343
            $uri->getPath(),
68✔
344
            $uri->getQuery(),
68✔
345
            $uri->getFragment()
68✔
346
        );
68✔
347
    }
348

349
    /**
350
     * Fires the actual cURL request.
351
     *
352
     * @return ResponseInterface
353
     */
354
    public function send(string $method, string $url)
355
    {
356
        // Reset our curl options so we're on a fresh slate.
357
        $curlOptions = [];
138✔
358

359
        if (! empty($this->config['query']) && is_array($this->config['query'])) {
138✔
360
            // This is likely too naive a solution.
361
            // Should look into handling when $url already
362
            // has query vars on it.
363
            $url .= '?' . http_build_query($this->config['query']);
2✔
364
            unset($this->config['query']);
2✔
365
        }
366

367
        $curlOptions[CURLOPT_URL]            = $url;
138✔
368
        $curlOptions[CURLOPT_RETURNTRANSFER] = true;
138✔
369
        $curlOptions[CURLOPT_HEADER]         = true;
138✔
370
        $curlOptions[CURLOPT_FRESH_CONNECT]  = true;
138✔
371
        // Disable @file uploads in post data.
372
        $curlOptions[CURLOPT_SAFE_UPLOAD] = true;
138✔
373

374
        $curlOptions = $this->setCURLOptions($curlOptions, $this->config);
138✔
375
        $curlOptions = $this->applyMethod($method, $curlOptions);
134✔
376
        $curlOptions = $this->applyRequestHeaders($curlOptions);
134✔
377

378
        // Do we need to delay this request?
379
        if ($this->delay > 0) {
134✔
380
            usleep((int) $this->delay * 1_000_000);
22✔
381
        }
382

383
        $output = $this->sendRequest($curlOptions);
134✔
384

385
        // Set the string we want to break our response from
386
        $breakString = "\r\n\r\n";
134✔
387

388
        while (str_starts_with($output, 'HTTP/1.1 100 Continue')) {
134✔
389
            $output = substr($output, strpos($output, $breakString) + 4);
6✔
390
        }
391

392
        if (preg_match('/HTTP\/\d\.\d 200 Connection established/i', $output)) {
134✔
393
            $output = substr($output, strpos($output, $breakString) + 4);
4✔
394
        }
395

396
        // If request and response have Digest
397
        if (isset($this->config['auth'][2]) && $this->config['auth'][2] === 'digest' && str_contains($output, 'WWW-Authenticate: Digest')) {
134✔
398
            $output = substr($output, strpos($output, $breakString) + 4);
4✔
399
        }
400

401
        // Split out our headers and body
402
        $break = strpos($output, $breakString);
134✔
403

404
        if ($break !== false) {
134✔
405
            // Our headers
406
            $headers = explode("\n", substr($output, 0, $break));
24✔
407

408
            $this->setResponseHeaders($headers);
24✔
409

410
            // Our body
411
            $body = substr($output, $break + 4);
24✔
412
            $this->response->setBody($body);
24✔
413
        } else {
414
            $this->response->setBody($output);
110✔
415
        }
416

417
        return $this->response;
134✔
418
    }
419

420
    /**
421
     * Adds $this->headers to the cURL request.
422
     */
423
    protected function applyRequestHeaders(array $curlOptions = []): array
424
    {
425
        if (empty($this->headers)) {
134✔
426
            return $curlOptions;
65✔
427
        }
428

429
        $set = [];
72✔
430

431
        foreach (array_keys($this->headers) as $name) {
72✔
432
            $set[] = $name . ': ' . $this->getHeaderLine($name);
72✔
433
        }
434

435
        $curlOptions[CURLOPT_HTTPHEADER] = $set;
72✔
436

437
        return $curlOptions;
72✔
438
    }
439

440
    /**
441
     * Apply method
442
     */
443
    protected function applyMethod(string $method, array $curlOptions): array
444
    {
445
        $this->method                       = $method;
134✔
446
        $curlOptions[CURLOPT_CUSTOMREQUEST] = $method;
134✔
447

448
        $size = strlen($this->body ?? '');
134✔
449

450
        // Have content?
451
        if ($size > 0) {
134✔
452
            return $this->applyBody($curlOptions);
10✔
453
        }
454

455
        if ($method === Method::PUT || $method === Method::POST) {
125✔
456
            // See http://tools.ietf.org/html/rfc7230#section-3.3.2
457
            if ($this->header('content-length') === null && ! isset($this->config['multipart'])) {
37✔
458
                $this->setHeader('Content-Length', '0');
25✔
459
            }
460
        } elseif ($method === 'HEAD') {
92✔
461
            $curlOptions[CURLOPT_NOBODY] = 1;
2✔
462
        }
463

464
        return $curlOptions;
125✔
465
    }
466

467
    /**
468
     * Apply body
469
     */
470
    protected function applyBody(array $curlOptions = []): array
471
    {
472
        if (! empty($this->body)) {
10✔
473
            $curlOptions[CURLOPT_POSTFIELDS] = (string) $this->getBody();
10✔
474
        }
475

476
        return $curlOptions;
10✔
477
    }
478

479
    /**
480
     * Parses the header retrieved from the cURL response into
481
     * our Response object.
482
     *
483
     * @return void
484
     */
485
    protected function setResponseHeaders(array $headers = [])
486
    {
487
        foreach ($headers as $header) {
24✔
488
            if (($pos = strpos($header, ':')) !== false) {
24✔
489
                $title = trim(substr($header, 0, $pos));
20✔
490
                $value = trim(substr($header, $pos + 1));
20✔
491

492
                if ($this->response instanceof Response) {
20✔
493
                    $this->response->addHeader($title, $value);
20✔
494
                } else {
495
                    $this->response->setHeader($title, $value);
×
496
                }
497
            } elseif (str_starts_with($header, 'HTTP')) {
22✔
498
                preg_match('#^HTTP\/([12](?:\.[01])?) (\d+) (.+)#', $header, $matches);
22✔
499

500
                if (isset($matches[1])) {
22✔
501
                    $this->response->setProtocolVersion($matches[1]);
18✔
502
                }
503

504
                if (isset($matches[2])) {
22✔
505
                    $this->response->setStatusCode((int) $matches[2], $matches[3] ?? null);
18✔
506
                }
507
            }
508
        }
509
    }
510

511
    /**
512
     * Set CURL options
513
     *
514
     * @return array
515
     *
516
     * @throws InvalidArgumentException
517
     */
518
    protected function setCURLOptions(array $curlOptions = [], array $config = [])
519
    {
520
        // Auth Headers
521
        if (! empty($config['auth'])) {
138✔
522
            $curlOptions[CURLOPT_USERPWD] = $config['auth'][0] . ':' . $config['auth'][1];
10✔
523

524
            if (! empty($config['auth'][2]) && strtolower($config['auth'][2]) === 'digest') {
10✔
525
                $curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_DIGEST;
4✔
526
            } else {
527
                $curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
6✔
528
            }
529
        }
530

531
        // Certificate
532
        if (! empty($config['cert'])) {
138✔
533
            $cert = $config['cert'];
6✔
534

535
            if (is_array($cert)) {
6✔
536
                $curlOptions[CURLOPT_SSLCERTPASSWD] = $cert[1];
2✔
537
                $cert                               = $cert[0];
2✔
538
            }
539

540
            if (! is_file($cert)) {
6✔
541
                throw HTTPException::forSSLCertNotFound($cert);
2✔
542
            }
543

544
            $curlOptions[CURLOPT_SSLCERT] = $cert;
4✔
545
        }
546

547
        // SSL Verification
548
        if (isset($config['verify'])) {
136✔
549
            if (is_string($config['verify'])) {
136✔
550
                $file = realpath($config['verify']) ?: $config['verify'];
4✔
551

552
                if (! is_file($file)) {
4✔
553
                    throw HTTPException::forInvalidSSLKey($config['verify']);
2✔
554
                }
555

556
                $curlOptions[CURLOPT_CAINFO]         = $file;
2✔
557
                $curlOptions[CURLOPT_SSL_VERIFYPEER] = true;
2✔
558
                $curlOptions[CURLOPT_SSL_VERIFYHOST] = 2;
2✔
559
            } elseif (is_bool($config['verify'])) {
132✔
560
                $curlOptions[CURLOPT_SSL_VERIFYPEER] = $config['verify'];
132✔
561
                $curlOptions[CURLOPT_SSL_VERIFYHOST] = $config['verify'] ? 2 : 0;
132✔
562
            }
563
        }
564

565
        // Proxy
566
        if (isset($config['proxy'])) {
134✔
567
            $curlOptions[CURLOPT_HTTPPROXYTUNNEL] = true;
2✔
568
            $curlOptions[CURLOPT_PROXY]           = $config['proxy'];
2✔
569
        }
570

571
        // Debug
572
        if ($config['debug']) {
134✔
573
            $curlOptions[CURLOPT_VERBOSE] = 1;
4✔
574
            $curlOptions[CURLOPT_STDERR]  = is_string($config['debug']) ? fopen($config['debug'], 'a+b') : fopen('php://stderr', 'wb');
4✔
575
        }
576

577
        // Decode Content
578
        if (! empty($config['decode_content'])) {
134✔
579
            $accept = $this->getHeaderLine('Accept-Encoding');
4✔
580

581
            if ($accept !== '') {
4✔
582
                $curlOptions[CURLOPT_ENCODING] = $accept;
2✔
583
            } else {
584
                $curlOptions[CURLOPT_ENCODING]   = '';
2✔
585
                $curlOptions[CURLOPT_HTTPHEADER] = 'Accept-Encoding';
2✔
586
            }
587
        }
588

589
        // Allow Redirects
590
        if (array_key_exists('allow_redirects', $config)) {
134✔
591
            $settings = $this->redirectDefaults;
8✔
592

593
            if (is_array($config['allow_redirects'])) {
8✔
594
                $settings = array_merge($settings, $config['allow_redirects']);
2✔
595
            }
596

597
            if ($config['allow_redirects'] === false) {
8✔
598
                $curlOptions[CURLOPT_FOLLOWLOCATION] = 0;
2✔
599
            } else {
600
                $curlOptions[CURLOPT_FOLLOWLOCATION] = 1;
6✔
601
                $curlOptions[CURLOPT_MAXREDIRS]      = $settings['max'];
6✔
602

603
                if ($settings['strict'] === true) {
6✔
604
                    $curlOptions[CURLOPT_POSTREDIR] = 1 | 2 | 4;
6✔
605
                }
606

607
                $protocols = 0;
6✔
608

609
                foreach ($settings['protocols'] as $proto) {
6✔
610
                    $protocols += constant('CURLPROTO_' . strtoupper($proto));
6✔
611
                }
612

613
                $curlOptions[CURLOPT_REDIR_PROTOCOLS] = $protocols;
6✔
614
            }
615
        }
616

617
        // Timeout
618
        $curlOptions[CURLOPT_TIMEOUT_MS] = (float) $config['timeout'] * 1000;
134✔
619

620
        // Connection Timeout
621
        $curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = (float) $config['connect_timeout'] * 1000;
134✔
622

623
        // Post Data - application/x-www-form-urlencoded
624
        if (! empty($config['form_params']) && is_array($config['form_params'])) {
134✔
625
            $postFields                      = http_build_query($config['form_params']);
10✔
626
            $curlOptions[CURLOPT_POSTFIELDS] = $postFields;
10✔
627

628
            // Ensure content-length is set, since CURL doesn't seem to
629
            // calculate it when HTTPHEADER is set.
630
            $this->setHeader('Content-Length', (string) strlen($postFields));
10✔
631
            $this->setHeader('Content-Type', 'application/x-www-form-urlencoded');
10✔
632
        }
633

634
        // Post Data - multipart/form-data
635
        if (! empty($config['multipart']) && is_array($config['multipart'])) {
134✔
636
            // setting the POSTFIELDS option automatically sets multipart
637
            $curlOptions[CURLOPT_POSTFIELDS] = $config['multipart'];
4✔
638
        }
639

640
        // HTTP Errors
641
        $curlOptions[CURLOPT_FAILONERROR] = array_key_exists('http_errors', $config) ? (bool) $config['http_errors'] : true;
134✔
642

643
        // JSON
644
        if (isset($config['json'])) {
134✔
645
            // Will be set as the body in `applyBody()`
646
            $json = json_encode($config['json']);
4✔
647
            $this->setBody($json);
4✔
648
            $this->setHeader('Content-Type', 'application/json');
4✔
649
            $this->setHeader('Content-Length', (string) strlen($json));
4✔
650
        }
651

652
        // Resolve IP
653
        if (array_key_exists('force_ip_resolve', $config)) {
134✔
654
            $curlOptions[CURLOPT_IPRESOLVE] = match ($config['force_ip_resolve']) {
6✔
655
                'v4'    => CURL_IPRESOLVE_V4,
2✔
656
                'v6'    => CURL_IPRESOLVE_V6,
2✔
657
                default => CURL_IPRESOLVE_WHATEVER
2✔
658
            };
6✔
659
        }
660

661
        // version
662
        if (! empty($config['version'])) {
134✔
663
            $version = sprintf('%.1F', $config['version']);
10✔
664
            if ($version === '1.0') {
10✔
665
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
4✔
666
            } elseif ($version === '1.1') {
8✔
667
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
4✔
668
            } elseif ($version === '2.0') {
6✔
669
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
4✔
670
            } elseif ($version === '3.0') {
2✔
671
                if (! defined('CURL_HTTP_VERSION_3')) {
2✔
672
                    define('CURL_HTTP_VERSION_3', 30);
1✔
673
                }
674

675
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_3;
2✔
676
            }
677
        }
678

679
        // Cookie
680
        if (isset($config['cookie'])) {
134✔
681
            $curlOptions[CURLOPT_COOKIEJAR]  = $config['cookie'];
2✔
682
            $curlOptions[CURLOPT_COOKIEFILE] = $config['cookie'];
2✔
683
        }
684

685
        // User Agent
686
        if (isset($config['user_agent'])) {
134✔
687
            $curlOptions[CURLOPT_USERAGENT] = $config['user_agent'];
4✔
688
        }
689

690
        return $curlOptions;
134✔
691
    }
692

693
    /**
694
     * Does the actual work of initializing cURL, setting the options,
695
     * and grabbing the output.
696
     *
697
     * @codeCoverageIgnore
698
     */
699
    protected function sendRequest(array $curlOptions = []): string
700
    {
701
        $ch = curl_init();
2✔
702

703
        curl_setopt_array($ch, $curlOptions);
2✔
704

705
        // Send the request and wait for a response.
706
        $output = curl_exec($ch);
2✔
707

708
        if ($output === false) {
2✔
709
            throw HTTPException::forCurlError((string) curl_errno($ch), curl_error($ch));
×
710
        }
711

712
        curl_close($ch);
2✔
713

714
        return $output;
2✔
715
    }
716
}
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

© 2025 Coveralls, Inc