CurlFactory::applyHandlerOptions()   F
last analyzed

Complexity

Conditions 38
Paths > 20000

Size

Total Lines 152

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 38
nc 1123231
nop 2
dl 0
loc 152
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace GuzzleHttp\Handler;
4
5
use GuzzleHttp\Exception\ConnectException;
6
use GuzzleHttp\Exception\RequestException;
7
use GuzzleHttp\Promise as P;
8
use GuzzleHttp\Promise\FulfilledPromise;
9
use GuzzleHttp\Promise\PromiseInterface;
10
use GuzzleHttp\Psr7\LazyOpenStream;
11
use GuzzleHttp\TransferStats;
12
use GuzzleHttp\Utils;
13
use Psr\Http\Message\RequestInterface;
14
15
/**
16
 * Creates curl resources from a request
17
 *
18
 * @final
19
 */
20
class CurlFactory implements CurlFactoryInterface
21
{
22
    public const CURL_VERSION_STR = 'curl_version';
23
24
    /**
25
     * @deprecated
26
     */
27
    public const LOW_CURL_VERSION_NUMBER = '7.21.2';
28
29
    /**
30
     * @var resource[]|\CurlHandle[]
31
     */
32
    private $handles = [];
33
34
    /**
35
     * @var int Total number of idle handles to keep in cache
36
     */
37
    private $maxHandles;
38
39
    /**
40
     * @param int $maxHandles Maximum number of idle handles.
41
     */
42
    public function __construct(int $maxHandles)
43
    {
44
        $this->maxHandles = $maxHandles;
45
    }
46
47
    public function create(RequestInterface $request, array $options): EasyHandle
48
    {
49
        if (isset($options['curl']['body_as_string'])) {
50
            $options['_body_as_string'] = $options['curl']['body_as_string'];
51
            unset($options['curl']['body_as_string']);
52
        }
53
54
        $easy = new EasyHandle;
55
        $easy->request = $request;
56
        $easy->options = $options;
57
        $conf = $this->getDefaultConf($easy);
58
        $this->applyMethod($easy, $conf);
59
        $this->applyHandlerOptions($easy, $conf);
60
        $this->applyHeaders($easy, $conf);
61
        unset($conf['_headers']);
62
63
        // Add handler options from the request configuration options
64
        if (isset($options['curl'])) {
65
            $conf = \array_replace($conf, $options['curl']);
66
        }
67
68
        $conf[\CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
69
        $easy->handle = $this->handles ? \array_pop($this->handles) : \curl_init();
70
        curl_setopt_array($easy->handle, $conf);
71
72
        return $easy;
73
    }
74
75
    public function release(EasyHandle $easy): void
76
    {
77
        $resource = $easy->handle;
78
        unset($easy->handle);
79
80
        if (\count($this->handles) >= $this->maxHandles) {
81
            \curl_close($resource);
82
        } else {
83
            // Remove all callback functions as they can hold onto references
84
            // and are not cleaned up by curl_reset. Using curl_setopt_array
85
            // does not work for some reason, so removing each one
86
            // individually.
87
            \curl_setopt($resource, \CURLOPT_HEADERFUNCTION, null);
88
            \curl_setopt($resource, \CURLOPT_READFUNCTION, null);
89
            \curl_setopt($resource, \CURLOPT_WRITEFUNCTION, null);
90
            \curl_setopt($resource, \CURLOPT_PROGRESSFUNCTION, null);
91
            \curl_reset($resource);
92
            $this->handles[] = $resource;
93
        }
94
    }
95
96
    /**
97
     * Completes a cURL transaction, either returning a response promise or a
98
     * rejected promise.
99
     *
100
     * @param callable(RequestInterface, array): PromiseInterface $handler
101
     * @param CurlFactoryInterface                                $factory Dictates how the handle is released
102
     */
103
    public static function finish(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface
104
    {
105
        if (isset($easy->options['on_stats'])) {
106
            self::invokeStats($easy);
107
        }
108
109
        if (!$easy->response || $easy->errno) {
110
            return self::finishError($handler, $easy, $factory);
111
        }
112
113
        // Return the response if it is present and there is no error.
114
        $factory->release($easy);
115
116
        // Rewind the body of the response if possible.
117
        $body = $easy->response->getBody();
118
        if ($body->isSeekable()) {
119
            $body->rewind();
120
        }
121
122
        return new FulfilledPromise($easy->response);
123
    }
124
125
    private static function invokeStats(EasyHandle $easy): void
126
    {
127
        $curlStats = \curl_getinfo($easy->handle);
128
        $curlStats['appconnect_time'] = \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME);
129
        $stats = new TransferStats(
130
            $easy->request,
131
            $easy->response,
132
            $curlStats['total_time'],
133
            $easy->errno,
134
            $curlStats
135
        );
136
        ($easy->options['on_stats'])($stats);
137
    }
138
139
    /**
140
     * @param callable(RequestInterface, array): PromiseInterface $handler
141
     */
142
    private static function finishError(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface
143
    {
144
        // Get error information and release the handle to the factory.
145
        $ctx = [
146
            'errno' => $easy->errno,
147
            'error' => \curl_error($easy->handle),
148
            'appconnect_time' => \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME),
149
        ] + \curl_getinfo($easy->handle);
150
        $ctx[self::CURL_VERSION_STR] = \curl_version()['version'];
151
        $factory->release($easy);
152
153
        // Retry when nothing is present or when curl failed to rewind.
154
        if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65)) {
155
            return self::retryFailedRewind($handler, $easy, $ctx);
156
        }
157
158
        return self::createRejection($easy, $ctx);
159
    }
160
161
    private static function createRejection(EasyHandle $easy, array $ctx): PromiseInterface
162
    {
163
        static $connectionErrors = [
164
            \CURLE_OPERATION_TIMEOUTED  => true,
165
            \CURLE_COULDNT_RESOLVE_HOST => true,
166
            \CURLE_COULDNT_CONNECT      => true,
167
            \CURLE_SSL_CONNECT_ERROR    => true,
168
            \CURLE_GOT_NOTHING          => true,
169
        ];
170
171
        if ($easy->createResponseException) {
172
            return P\Create::rejectionFor(
173
                new RequestException(
174
                    'An error was encountered while creating the response',
175
                    $easy->request,
176
                    $easy->response,
177
                    $easy->createResponseException,
178
                    $ctx
179
                )
180
            );
181
        }
182
183
        // If an exception was encountered during the onHeaders event, then
184
        // return a rejected promise that wraps that exception.
185
        if ($easy->onHeadersException) {
186
            return P\Create::rejectionFor(
187
                new RequestException(
188
                    'An error was encountered during the on_headers event',
189
                    $easy->request,
190
                    $easy->response,
191
                    $easy->onHeadersException,
192
                    $ctx
193
                )
194
            );
195
        }
196
197
        $message = \sprintf(
198
            'cURL error %s: %s (%s)',
199
            $ctx['errno'],
200
            $ctx['error'],
201
            'see https://curl.haxx.se/libcurl/c/libcurl-errors.html'
202
        );
203
        $uriString = (string) $easy->request->getUri();
204
        if ($uriString !== '' && false === \strpos($ctx['error'], $uriString)) {
205
            $message .= \sprintf(' for %s', $uriString);
206
        }
207
208
        // Create a connection exception if it was a specific error code.
209
        $error = isset($connectionErrors[$easy->errno])
210
            ? new ConnectException($message, $easy->request, null, $ctx)
211
            : new RequestException($message, $easy->request, $easy->response, null, $ctx);
212
213
        return P\Create::rejectionFor($error);
214
    }
215
216
    /**
217
     * @return array<int|string, mixed>
0 ignored issues
show
Documentation introduced by
The doc-type array<int|string, could not be parsed: Expected ">" at position 7, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
218
     */
219
    private function getDefaultConf(EasyHandle $easy): array
220
    {
221
        $conf = [
222
            '_headers'              => $easy->request->getHeaders(),
223
            \CURLOPT_CUSTOMREQUEST  => $easy->request->getMethod(),
224
            \CURLOPT_URL            => (string) $easy->request->getUri()->withFragment(''),
225
            \CURLOPT_RETURNTRANSFER => false,
226
            \CURLOPT_HEADER         => false,
227
            \CURLOPT_CONNECTTIMEOUT => 150,
228
        ];
229
230
        if (\defined('CURLOPT_PROTOCOLS')) {
231
            $conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP | \CURLPROTO_HTTPS;
232
        }
233
234
        $version = $easy->request->getProtocolVersion();
235
        if ($version == 1.1) {
236
            $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
237
        } elseif ($version == 2.0) {
238
            $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
239
        } else {
240
            $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
241
        }
242
243
        return $conf;
244
    }
245
246
    private function applyMethod(EasyHandle $easy, array &$conf): void
247
    {
248
        $body = $easy->request->getBody();
249
        $size = $body->getSize();
250
251
        if ($size === null || $size > 0) {
252
            $this->applyBody($easy->request, $easy->options, $conf);
253
            return;
254
        }
255
256
        $method = $easy->request->getMethod();
257
        if ($method === 'PUT' || $method === 'POST') {
258
            // See https://tools.ietf.org/html/rfc7230#section-3.3.2
259
            if (!$easy->request->hasHeader('Content-Length')) {
260
                $conf[\CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
261
            }
262
        } elseif ($method === 'HEAD') {
263
            $conf[\CURLOPT_NOBODY] = true;
264
            unset(
265
                $conf[\CURLOPT_WRITEFUNCTION],
266
                $conf[\CURLOPT_READFUNCTION],
267
                $conf[\CURLOPT_FILE],
268
                $conf[\CURLOPT_INFILE]
269
            );
270
        }
271
    }
272
273
    private function applyBody(RequestInterface $request, array $options, array &$conf): void
274
    {
275
        $size = $request->hasHeader('Content-Length')
276
            ? (int) $request->getHeaderLine('Content-Length')
277
            : null;
278
279
        // Send the body as a string if the size is less than 1MB OR if the
280
        // [curl][body_as_string] request value is set.
281
        if (($size !== null && $size < 1000000) || !empty($options['_body_as_string'])) {
282
            $conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody();
283
            // Don't duplicate the Content-Length header
284
            $this->removeHeader('Content-Length', $conf);
285
            $this->removeHeader('Transfer-Encoding', $conf);
286
        } else {
287
            $conf[\CURLOPT_UPLOAD] = true;
288
            if ($size !== null) {
289
                $conf[\CURLOPT_INFILESIZE] = $size;
290
                $this->removeHeader('Content-Length', $conf);
291
            }
292
            $body = $request->getBody();
293
            if ($body->isSeekable()) {
294
                $body->rewind();
295
            }
296
            $conf[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body) {
297
                return $body->read($length);
298
            };
299
        }
300
301
        // If the Expect header is not present, prevent curl from adding it
302
        if (!$request->hasHeader('Expect')) {
303
            $conf[\CURLOPT_HTTPHEADER][] = 'Expect:';
304
        }
305
306
        // cURL sometimes adds a content-type by default. Prevent this.
307
        if (!$request->hasHeader('Content-Type')) {
308
            $conf[\CURLOPT_HTTPHEADER][] = 'Content-Type:';
309
        }
310
    }
311
312
    private function applyHeaders(EasyHandle $easy, array &$conf): void
313
    {
314
        foreach ($conf['_headers'] as $name => $values) {
315
            foreach ($values as $value) {
316
                $value = (string) $value;
317
                if ($value === '') {
318
                    // cURL requires a special format for empty headers.
319
                    // See https://github.com/guzzle/guzzle/issues/1882 for more details.
320
                    $conf[\CURLOPT_HTTPHEADER][] = "$name;";
321
                } else {
322
                    $conf[\CURLOPT_HTTPHEADER][] = "$name: $value";
323
                }
324
            }
325
        }
326
327
        // Remove the Accept header if one was not set
328
        if (!$easy->request->hasHeader('Accept')) {
329
            $conf[\CURLOPT_HTTPHEADER][] = 'Accept:';
330
        }
331
    }
332
333
    /**
334
     * Remove a header from the options array.
335
     *
336
     * @param string $name    Case-insensitive header to remove
337
     * @param array  $options Array of options to modify
338
     */
339
    private function removeHeader(string $name, array &$options): void
340
    {
341
        foreach (\array_keys($options['_headers']) as $key) {
342
            if (!\strcasecmp($key, $name)) {
343
                unset($options['_headers'][$key]);
344
                return;
345
            }
346
        }
347
    }
348
349
    private function applyHandlerOptions(EasyHandle $easy, array &$conf): void
350
    {
351
        $options = $easy->options;
352
        if (isset($options['verify'])) {
353
            if ($options['verify'] === false) {
354
                unset($conf[\CURLOPT_CAINFO]);
355
                $conf[\CURLOPT_SSL_VERIFYHOST] = 0;
356
                $conf[\CURLOPT_SSL_VERIFYPEER] = false;
357
            } else {
358
                $conf[\CURLOPT_SSL_VERIFYHOST] = 2;
359
                $conf[\CURLOPT_SSL_VERIFYPEER] = true;
360
                if (\is_string($options['verify'])) {
361
                    // Throw an error if the file/folder/link path is not valid or doesn't exist.
362
                    if (!\file_exists($options['verify'])) {
363
                        throw new \InvalidArgumentException("SSL CA bundle not found: {$options['verify']}");
364
                    }
365
                    // If it's a directory or a link to a directory use CURLOPT_CAPATH.
366
                    // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO.
367
                    if (
368
                        \is_dir($options['verify']) ||
369
                        (
370
                            \is_link($options['verify']) === true &&
371
                            ($verifyLink = \readlink($options['verify'])) !== false &&
372
                            \is_dir($verifyLink)
0 ignored issues
show
Bug introduced by
The variable $verifyLink does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
373
                        )
374
                    ) {
375
                        $conf[\CURLOPT_CAPATH] = $options['verify'];
376
                    } else {
377
                        $conf[\CURLOPT_CAINFO] = $options['verify'];
378
                    }
379
                }
380
            }
381
        }
382
383
        if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) {
384
            $accept = $easy->request->getHeaderLine('Accept-Encoding');
385
            if ($accept) {
386
                $conf[\CURLOPT_ENCODING] = $accept;
387
            } else {
388
                $conf[\CURLOPT_ENCODING] = '';
389
                // Don't let curl send the header over the wire
390
                $conf[\CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
391
            }
392
        }
393
394
        if (!isset($options['sink'])) {
395
            // Use a default temp stream if no sink was set.
396
            $options['sink'] = \GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'w+');
397
        }
398
        $sink = $options['sink'];
399
        if (!\is_string($sink)) {
400
            $sink = \GuzzleHttp\Psr7\Utils::streamFor($sink);
401
        } elseif (!\is_dir(\dirname($sink))) {
402
            // Ensure that the directory exists before failing in curl.
403
            throw new \RuntimeException(\sprintf('Directory %s does not exist for sink value of %s', \dirname($sink), $sink));
404
        } else {
405
            $sink = new LazyOpenStream($sink, 'w+');
406
        }
407
        $easy->sink = $sink;
408
        $conf[\CURLOPT_WRITEFUNCTION] = static function ($ch, $write) use ($sink): int {
409
            return $sink->write($write);
410
        };
411
412
        $timeoutRequiresNoSignal = false;
413
        if (isset($options['timeout'])) {
414
            $timeoutRequiresNoSignal |= $options['timeout'] < 1;
415
            $conf[\CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
416
        }
417
418
        // CURL default value is CURL_IPRESOLVE_WHATEVER
419
        if (isset($options['force_ip_resolve'])) {
420
            if ('v4' === $options['force_ip_resolve']) {
421
                $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4;
422
            } elseif ('v6' === $options['force_ip_resolve']) {
423
                $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V6;
424
            }
425
        }
426
427
        if (isset($options['connect_timeout'])) {
428
            $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1;
429
            $conf[\CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
430
        }
431
432
        if ($timeoutRequiresNoSignal && \strtoupper(\substr(\PHP_OS, 0, 3)) !== 'WIN') {
433
            $conf[\CURLOPT_NOSIGNAL] = true;
434
        }
435
436
        if (isset($options['proxy'])) {
437
            if (!\is_array($options['proxy'])) {
438
                $conf[\CURLOPT_PROXY] = $options['proxy'];
439
            } else {
440
                $scheme = $easy->request->getUri()->getScheme();
441
                if (isset($options['proxy'][$scheme])) {
442
                    $host = $easy->request->getUri()->getHost();
443
                    if (!isset($options['proxy']['no']) || !Utils::isHostInNoProxy($host, $options['proxy']['no'])) {
444
                        $conf[\CURLOPT_PROXY] = $options['proxy'][$scheme];
445
                    }
446
                }
447
            }
448
        }
449
450
        if (isset($options['cert'])) {
451
            $cert = $options['cert'];
452
            if (\is_array($cert)) {
453
                $conf[\CURLOPT_SSLCERTPASSWD] = $cert[1];
454
                $cert = $cert[0];
455
            }
456
            if (!\file_exists($cert)) {
457
                throw new \InvalidArgumentException("SSL certificate not found: {$cert}");
458
            }
459
            # OpenSSL (versions 0.9.3 and later) also support "P12" for PKCS#12-encoded files.
460
            # see https://curl.se/libcurl/c/CURLOPT_SSLCERTTYPE.html
461
            $ext = pathinfo($cert, \PATHINFO_EXTENSION);
462
            if (preg_match('#^(der|p12)$#i', $ext)) {
463
                $conf[\CURLOPT_SSLCERTTYPE] = strtoupper($ext);
464
            }
465
            $conf[\CURLOPT_SSLCERT] = $cert;
466
        }
467
468
        if (isset($options['ssl_key'])) {
469
            if (\is_array($options['ssl_key'])) {
470
                if (\count($options['ssl_key']) === 2) {
471
                    [$sslKey, $conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key'];
0 ignored issues
show
Bug introduced by
The variable $sslKey seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
472
                } else {
473
                    [$sslKey] = $options['ssl_key'];
0 ignored issues
show
Bug introduced by
The variable $sslKey seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
474
                }
475
            }
476
477
            $sslKey = $sslKey ?? $options['ssl_key'];
478
479
            if (!\file_exists($sslKey)) {
480
                throw new \InvalidArgumentException("SSL private key not found: {$sslKey}");
481
            }
482
            $conf[\CURLOPT_SSLKEY] = $sslKey;
483
        }
484
485
        if (isset($options['progress'])) {
486
            $progress = $options['progress'];
487
            if (!\is_callable($progress)) {
488
                throw new \InvalidArgumentException('progress client option must be callable');
489
            }
490
            $conf[\CURLOPT_NOPROGRESS] = false;
491
            $conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use ($progress) {
492
                $progress($downloadSize, $downloaded, $uploadSize, $uploaded);
493
            };
494
        }
495
496
        if (!empty($options['debug'])) {
497
            $conf[\CURLOPT_STDERR] = Utils::debugResource($options['debug']);
498
            $conf[\CURLOPT_VERBOSE] = true;
499
        }
500
    }
501
502
    /**
503
     * This function ensures that a response was set on a transaction. If one
504
     * was not set, then the request is retried if possible. This error
505
     * typically means you are sending a payload, curl encountered a
506
     * "Connection died, retrying a fresh connect" error, tried to rewind the
507
     * stream, and then encountered a "necessary data rewind wasn't possible"
508
     * error, causing the request to be sent through curl_multi_info_read()
509
     * without an error status.
510
     *
511
     * @param callable(RequestInterface, array): PromiseInterface $handler
512
     */
513
    private static function retryFailedRewind(callable $handler, EasyHandle $easy, array $ctx): PromiseInterface
514
    {
515
        try {
516
            // Only rewind if the body has been read from.
517
            $body = $easy->request->getBody();
518
            if ($body->tell() > 0) {
519
                $body->rewind();
520
            }
521
        } catch (\RuntimeException $e) {
522
            $ctx['error'] = 'The connection unexpectedly failed without '
523
                . 'providing an error. The request would have been retried, '
524
                . 'but attempting to rewind the request body failed. '
525
                . 'Exception: ' . $e;
526
            return self::createRejection($easy, $ctx);
527
        }
528
529
        // Retry no more than 3 times before giving up.
530
        if (!isset($easy->options['_curl_retries'])) {
531
            $easy->options['_curl_retries'] = 1;
532
        } elseif ($easy->options['_curl_retries'] == 2) {
533
            $ctx['error'] = 'The cURL request was retried 3 times '
534
                . 'and did not succeed. The most likely reason for the failure '
535
                . 'is that cURL was unable to rewind the body of the request '
536
                . 'and subsequent retries resulted in the same error. Turn on '
537
                . 'the debug option to see what went wrong. See '
538
                . 'https://bugs.php.net/bug.php?id=47204 for more information.';
539
            return self::createRejection($easy, $ctx);
540
        } else {
541
            $easy->options['_curl_retries']++;
542
        }
543
544
        return $handler($easy->request, $easy->options);
545
    }
546
547
    private function createHeaderFn(EasyHandle $easy): callable
548
    {
549
        if (isset($easy->options['on_headers'])) {
550
            $onHeaders = $easy->options['on_headers'];
551
552
            if (!\is_callable($onHeaders)) {
553
                throw new \InvalidArgumentException('on_headers must be callable');
554
            }
555
        } else {
556
            $onHeaders = null;
557
        }
558
559
        return static function ($ch, $h) use (
560
            $onHeaders,
561
            $easy,
562
            &$startingResponse
563
        ) {
564
            $value = \trim($h);
565
            if ($value === '') {
566
                $startingResponse = true;
567
                try {
568
                    $easy->createResponse();
569
                } catch (\Exception $e) {
570
                    $easy->createResponseException = $e;
571
                    return -1;
572
                }
573
                if ($onHeaders !== null) {
574
                    try {
575
                        $onHeaders($easy->response);
576
                    } catch (\Exception $e) {
577
                        // Associate the exception with the handle and trigger
578
                        // a curl header write error by returning 0.
579
                        $easy->onHeadersException = $e;
580
                        return -1;
581
                    }
582
                }
583
            } elseif ($startingResponse) {
584
                $startingResponse = false;
585
                $easy->headers = [$value];
586
            } else {
587
                $easy->headers[] = $value;
588
            }
589
            return \strlen($h);
590
        };
591
    }
592
}
593