CurlFactory   F
last analyzed

Complexity

Total Complexity 94

Size/Duplication

Total Lines 541
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 294
c 0
b 0
f 0
dl 0
loc 541
rs 2
wmc 94

15 Methods

Rating   Name   Duplication   Size   Complexity  
A retryFailedRewind() 0 35 5
B applyMethod() 0 23 7
A applyHeaders() 0 11 4
A removeHeader() 0 6 3
A finishError() 0 20 4
A create() 0 28 4
A invokeStats() 0 11 1
A getDefaultConf() 0 25 4
B createHeaderFn() 0 38 7
A __construct() 0 3 1
A createRejection() 0 37 3
F applyHandlerOptions() 0 153 35
A finish() 0 23 5
B applyBody() 0 38 9
A release() 0 18 2

How to fix   Complexity   

Complex Class

Complex classes like CurlFactory often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CurlFactory, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace GuzzleHttp\Handler;
3
4
use GuzzleHttp\Exception\RequestException;
5
use GuzzleHttp\Exception\ConnectException;
6
use GuzzleHttp\Promise\FulfilledPromise;
7
use GuzzleHttp\Promise\RejectedPromise;
8
use GuzzleHttp\Psr7;
9
use GuzzleHttp\Psr7\LazyOpenStream;
10
use GuzzleHttp\TransferStats;
11
use Psr\Http\Message\RequestInterface;
12
13
/**
14
 * Creates curl resources from a request
15
 */
16
class CurlFactory implements CurlFactoryInterface
17
{
18
    /** @var array */
19
    private $handles = [];
20
21
    /** @var int Total number of idle handles to keep in cache */
22
    private $maxHandles;
23
24
    /**
25
     * @param int $maxHandles Maximum number of idle handles.
26
     */
27
    public function __construct($maxHandles)
28
    {
29
        $this->maxHandles = $maxHandles;
30
    }
31
32
    public function create(RequestInterface $request, array $options)
33
    {
34
        if (isset($options['curl']['body_as_string'])) {
35
            $options['_body_as_string'] = $options['curl']['body_as_string'];
36
            unset($options['curl']['body_as_string']);
37
        }
38
39
        $easy = new EasyHandle;
40
        $easy->request = $request;
41
        $easy->options = $options;
42
        $conf = $this->getDefaultConf($easy);
43
        $this->applyMethod($easy, $conf);
44
        $this->applyHandlerOptions($easy, $conf);
45
        $this->applyHeaders($easy, $conf);
46
        unset($conf['_headers']);
47
48
        // Add handler options from the request configuration options
49
        if (isset($options['curl'])) {
50
            $conf = array_replace($conf, $options['curl']);
51
        }
52
53
        $conf[CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
54
        $easy->handle = $this->handles
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->handles ? array_p...>handles) : curl_init() can also be of type CurlHandle. However, the property $handle is declared as type resource. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
55
            ? array_pop($this->handles)
56
            : curl_init();
57
        curl_setopt_array($easy->handle, $conf);
58
59
        return $easy;
60
    }
61
62
    public function release(EasyHandle $easy)
63
    {
64
        $resource = $easy->handle;
65
        unset($easy->handle);
66
67
        if (count($this->handles) >= $this->maxHandles) {
68
            curl_close($resource);
69
        } else {
70
            // Remove all callback functions as they can hold onto references
71
            // and are not cleaned up by curl_reset. Using curl_setopt_array
72
            // does not work for some reason, so removing each one
73
            // individually.
74
            curl_setopt($resource, CURLOPT_HEADERFUNCTION, null);
75
            curl_setopt($resource, CURLOPT_READFUNCTION, null);
76
            curl_setopt($resource, CURLOPT_WRITEFUNCTION, null);
77
            curl_setopt($resource, CURLOPT_PROGRESSFUNCTION, null);
78
            curl_reset($resource);
79
            $this->handles[] = $resource;
80
        }
81
    }
82
83
    /**
84
     * Completes a cURL transaction, either returning a response promise or a
85
     * rejected promise.
86
     *
87
     * @param callable             $handler
88
     * @param EasyHandle           $easy
89
     * @param CurlFactoryInterface $factory Dictates how the handle is released
90
     *
91
     * @return \GuzzleHttp\Promise\PromiseInterface
92
     */
93
    public static function finish(
94
        callable $handler,
95
        EasyHandle $easy,
96
        CurlFactoryInterface $factory
97
    ) {
98
        if (isset($easy->options['on_stats'])) {
99
            self::invokeStats($easy);
100
        }
101
102
        if (!$easy->response || $easy->errno) {
103
            return self::finishError($handler, $easy, $factory);
104
        }
105
106
        // Return the response if it is present and there is no error.
107
        $factory->release($easy);
108
109
        // Rewind the body of the response if possible.
110
        $body = $easy->response->getBody();
111
        if ($body->isSeekable()) {
112
            $body->rewind();
113
        }
114
115
        return new FulfilledPromise($easy->response);
116
    }
117
118
    private static function invokeStats(EasyHandle $easy)
119
    {
120
        $curlStats = curl_getinfo($easy->handle);
121
        $stats = new TransferStats(
122
            $easy->request,
123
            $easy->response,
124
            $curlStats['total_time'],
125
            $easy->errno,
126
            $curlStats
127
        );
128
        call_user_func($easy->options['on_stats'], $stats);
129
    }
130
131
    private static function finishError(
132
        callable $handler,
133
        EasyHandle $easy,
134
        CurlFactoryInterface $factory
135
    ) {
136
        // Get error information and release the handle to the factory.
137
        $ctx = [
138
            'errno' => $easy->errno,
139
            'error' => curl_error($easy->handle),
140
        ] + curl_getinfo($easy->handle);
141
        $factory->release($easy);
142
143
        // Retry when nothing is present or when curl failed to rewind.
144
        if (empty($easy->options['_err_message'])
145
            && (!$easy->errno || $easy->errno == 65)
146
        ) {
147
            return self::retryFailedRewind($handler, $easy, $ctx);
148
        }
149
150
        return self::createRejection($easy, $ctx);
151
    }
152
153
    private static function createRejection(EasyHandle $easy, array $ctx)
154
    {
155
        static $connectionErrors = [
156
            CURLE_OPERATION_TIMEOUTED  => true,
157
            CURLE_COULDNT_RESOLVE_HOST => true,
158
            CURLE_COULDNT_CONNECT      => true,
159
            CURLE_SSL_CONNECT_ERROR    => true,
160
            CURLE_GOT_NOTHING          => true,
161
        ];
162
163
        // If an exception was encountered during the onHeaders event, then
164
        // return a rejected promise that wraps that exception.
165
        if ($easy->onHeadersException) {
166
            return \GuzzleHttp\Promise\rejection_for(
0 ignored issues
show
Deprecated Code introduced by
The function GuzzleHttp\Promise\rejection_for() has been deprecated: rejection_for will be removed in guzzlehttp/promises:2.0. Use Create::rejectionFor instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

166
            return /** @scrutinizer ignore-deprecated */ \GuzzleHttp\Promise\rejection_for(

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
167
                new RequestException(
168
                    'An error was encountered during the on_headers event',
169
                    $easy->request,
170
                    $easy->response,
171
                    $easy->onHeadersException,
172
                    $ctx
173
                )
174
            );
175
        }
176
177
        $message = sprintf(
178
            'cURL error %s: %s (%s)',
179
            $ctx['errno'],
180
            $ctx['error'],
181
            'see http://curl.haxx.se/libcurl/c/libcurl-errors.html'
182
        );
183
184
        // Create a connection exception if it was a specific error code.
185
        $error = isset($connectionErrors[$easy->errno])
186
            ? new ConnectException($message, $easy->request, null, $ctx)
187
            : new RequestException($message, $easy->request, $easy->response, null, $ctx);
188
189
        return \GuzzleHttp\Promise\rejection_for($error);
0 ignored issues
show
Deprecated Code introduced by
The function GuzzleHttp\Promise\rejection_for() has been deprecated: rejection_for will be removed in guzzlehttp/promises:2.0. Use Create::rejectionFor instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

189
        return /** @scrutinizer ignore-deprecated */ \GuzzleHttp\Promise\rejection_for($error);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
190
    }
191
192
    private function getDefaultConf(EasyHandle $easy)
193
    {
194
        $conf = [
195
            '_headers'             => $easy->request->getHeaders(),
196
            CURLOPT_CUSTOMREQUEST  => $easy->request->getMethod(),
197
            CURLOPT_URL            => (string) $easy->request->getUri()->withFragment(''),
198
            CURLOPT_RETURNTRANSFER => false,
199
            CURLOPT_HEADER         => false,
200
            CURLOPT_CONNECTTIMEOUT => 150,
201
        ];
202
203
        if (defined('CURLOPT_PROTOCOLS')) {
204
            $conf[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
205
        }
206
207
        $version = $easy->request->getProtocolVersion();
208
        if ($version == 1.1) {
209
            $conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
210
        } elseif ($version == 2.0) {
211
            $conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
212
        } else {
213
            $conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
214
        }
215
216
        return $conf;
217
    }
218
219
    private function applyMethod(EasyHandle $easy, array &$conf)
220
    {
221
        $body = $easy->request->getBody();
222
        $size = $body->getSize();
223
224
        if ($size === null || $size > 0) {
225
            $this->applyBody($easy->request, $easy->options, $conf);
226
            return;
227
        }
228
229
        $method = $easy->request->getMethod();
230
        if ($method === 'PUT' || $method === 'POST') {
231
            // See http://tools.ietf.org/html/rfc7230#section-3.3.2
232
            if (!$easy->request->hasHeader('Content-Length')) {
233
                $conf[CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
234
            }
235
        } elseif ($method === 'HEAD') {
236
            $conf[CURLOPT_NOBODY] = true;
237
            unset(
238
                $conf[CURLOPT_WRITEFUNCTION],
239
                $conf[CURLOPT_READFUNCTION],
240
                $conf[CURLOPT_FILE],
241
                $conf[CURLOPT_INFILE]
242
            );
243
        }
244
    }
245
246
    private function applyBody(RequestInterface $request, array $options, array &$conf)
247
    {
248
        $size = $request->hasHeader('Content-Length')
249
            ? (int) $request->getHeaderLine('Content-Length')
250
            : null;
251
252
        // Send the body as a string if the size is less than 1MB OR if the
253
        // [curl][body_as_string] request value is set.
254
        if (($size !== null && $size < 1000000) ||
255
            !empty($options['_body_as_string'])
256
        ) {
257
            $conf[CURLOPT_POSTFIELDS] = (string) $request->getBody();
258
            // Don't duplicate the Content-Length header
259
            $this->removeHeader('Content-Length', $conf);
260
            $this->removeHeader('Transfer-Encoding', $conf);
261
        } else {
262
            $conf[CURLOPT_UPLOAD] = true;
263
            if ($size !== null) {
264
                $conf[CURLOPT_INFILESIZE] = $size;
265
                $this->removeHeader('Content-Length', $conf);
266
            }
267
            $body = $request->getBody();
268
            if ($body->isSeekable()) {
269
                $body->rewind();
270
            }
271
            $conf[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) {
272
                return $body->read($length);
273
            };
274
        }
275
276
        // If the Expect header is not present, prevent curl from adding it
277
        if (!$request->hasHeader('Expect')) {
278
            $conf[CURLOPT_HTTPHEADER][] = 'Expect:';
279
        }
280
281
        // cURL sometimes adds a content-type by default. Prevent this.
282
        if (!$request->hasHeader('Content-Type')) {
283
            $conf[CURLOPT_HTTPHEADER][] = 'Content-Type:';
284
        }
285
    }
286
287
    private function applyHeaders(EasyHandle $easy, array &$conf)
288
    {
289
        foreach ($conf['_headers'] as $name => $values) {
290
            foreach ($values as $value) {
291
                $conf[CURLOPT_HTTPHEADER][] = "$name: $value";
292
            }
293
        }
294
295
        // Remove the Accept header if one was not set
296
        if (!$easy->request->hasHeader('Accept')) {
297
            $conf[CURLOPT_HTTPHEADER][] = 'Accept:';
298
        }
299
    }
300
301
    /**
302
     * Remove a header from the options array.
303
     *
304
     * @param string $name    Case-insensitive header to remove
305
     * @param array  $options Array of options to modify
306
     */
307
    private function removeHeader($name, array &$options)
308
    {
309
        foreach (array_keys($options['_headers']) as $key) {
310
            if (!strcasecmp($key, $name)) {
311
                unset($options['_headers'][$key]);
312
                return;
313
            }
314
        }
315
    }
316
317
    private function applyHandlerOptions(EasyHandle $easy, array &$conf)
318
    {
319
        $options = $easy->options;
320
        if (isset($options['verify'])) {
321
            if ($options['verify'] === false) {
322
                unset($conf[CURLOPT_CAINFO]);
323
                $conf[CURLOPT_SSL_VERIFYHOST] = 0;
324
                $conf[CURLOPT_SSL_VERIFYPEER] = false;
325
            } else {
326
                $conf[CURLOPT_SSL_VERIFYHOST] = 2;
327
                $conf[CURLOPT_SSL_VERIFYPEER] = true;
328
                if (is_string($options['verify'])) {
329
                    // Throw an error if the file/folder/link path is not valid or doesn't exist.
330
                    if (!file_exists($options['verify'])) {
331
                        throw new \InvalidArgumentException(
332
                            "SSL CA bundle not found: {$options['verify']}"
333
                        );
334
                    }
335
                    // If it's a directory or a link to a directory use CURLOPT_CAPATH.
336
                    // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO.
337
                    if (is_dir($options['verify']) ||
338
                        (is_link($options['verify']) && is_dir(readlink($options['verify'])))) {
339
                        $conf[CURLOPT_CAPATH] = $options['verify'];
340
                    } else {
341
                        $conf[CURLOPT_CAINFO] = $options['verify'];
342
                    }
343
                }
344
            }
345
        }
346
347
        if (!empty($options['decode_content'])) {
348
            $accept = $easy->request->getHeaderLine('Accept-Encoding');
349
            if ($accept) {
350
                $conf[CURLOPT_ENCODING] = $accept;
351
            } else {
352
                $conf[CURLOPT_ENCODING] = '';
353
                // Don't let curl send the header over the wire
354
                $conf[CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
355
            }
356
        }
357
358
        if (isset($options['sink'])) {
359
            $sink = $options['sink'];
360
            if (!is_string($sink)) {
361
                $sink = \GuzzleHttp\Psr7\stream_for($sink);
0 ignored issues
show
Deprecated Code introduced by
The function GuzzleHttp\Psr7\stream_for() has been deprecated: stream_for will be removed in guzzlehttp/psr7:2.0. Use Utils::streamFor instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

361
                $sink = /** @scrutinizer ignore-deprecated */ \GuzzleHttp\Psr7\stream_for($sink);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
362
            } elseif (!is_dir(dirname($sink))) {
363
                // Ensure that the directory exists before failing in curl.
364
                throw new \RuntimeException(sprintf(
365
                    'Directory %s does not exist for sink value of %s',
366
                    dirname($sink),
367
                    $sink
368
                ));
369
            } else {
370
                $sink = new LazyOpenStream($sink, 'w+');
371
            }
372
            $easy->sink = $sink;
373
            $conf[CURLOPT_WRITEFUNCTION] = function ($ch, $write) use ($sink) {
374
                return $sink->write($write);
375
            };
376
        } else {
377
            // Use a default temp stream if no sink was set.
378
            $conf[CURLOPT_FILE] = fopen('php://temp', 'w+');
379
            $easy->sink = Psr7\stream_for($conf[CURLOPT_FILE]);
0 ignored issues
show
Bug introduced by
The function stream_for was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

379
            $easy->sink = /** @scrutinizer ignore-call */ Psr7\stream_for($conf[CURLOPT_FILE]);
Loading history...
380
        }
381
        $timeoutRequiresNoSignal = false;
382
        if (isset($options['timeout'])) {
383
            $timeoutRequiresNoSignal |= $options['timeout'] < 1;
384
            $conf[CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
385
        }
386
387
        // CURL default value is CURL_IPRESOLVE_WHATEVER
388
        if (isset($options['force_ip_resolve'])) {
389
            if ('v4' === $options['force_ip_resolve']) {
390
                $conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4;
391
            } else if ('v6' === $options['force_ip_resolve']) {
392
                $conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V6;
393
            }
394
        }
395
396
        if (isset($options['connect_timeout'])) {
397
            $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1;
398
            $conf[CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
399
        }
400
401
        if ($timeoutRequiresNoSignal && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
0 ignored issues
show
Bug Best Practice introduced by
The expression $timeoutRequiresNoSignal of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
402
            $conf[CURLOPT_NOSIGNAL] = true;
403
        }
404
405
        if (isset($options['proxy'])) {
406
            if (!is_array($options['proxy'])) {
407
                $conf[CURLOPT_PROXY] = $options['proxy'];
408
            } else {
409
                $scheme = $easy->request->getUri()->getScheme();
410
                if (isset($options['proxy'][$scheme])) {
411
                    $host = $easy->request->getUri()->getHost();
412
                    if (!isset($options['proxy']['no']) ||
413
                        !\GuzzleHttp\is_host_in_noproxy($host, $options['proxy']['no'])
414
                    ) {
415
                        $conf[CURLOPT_PROXY] = $options['proxy'][$scheme];
416
                    }
417
                }
418
            }
419
        }
420
421
        if (isset($options['cert'])) {
422
            $cert = $options['cert'];
423
            if (is_array($cert)) {
424
                $conf[CURLOPT_SSLCERTPASSWD] = $cert[1];
425
                $cert = $cert[0];
426
            }
427
            if (!file_exists($cert)) {
428
                throw new \InvalidArgumentException(
429
                    "SSL certificate not found: {$cert}"
430
                );
431
            }
432
            $conf[CURLOPT_SSLCERT] = $cert;
433
        }
434
435
        if (isset($options['ssl_key'])) {
436
            $sslKey = $options['ssl_key'];
437
            if (is_array($sslKey)) {
438
                $conf[CURLOPT_SSLKEYPASSWD] = $sslKey[1];
439
                $sslKey = $sslKey[0];
440
            }
441
            if (!file_exists($sslKey)) {
442
                throw new \InvalidArgumentException(
443
                    "SSL private key not found: {$sslKey}"
444
                );
445
            }
446
            $conf[CURLOPT_SSLKEY] = $sslKey;
447
        }
448
449
        if (isset($options['progress'])) {
450
            $progress = $options['progress'];
451
            if (!is_callable($progress)) {
452
                throw new \InvalidArgumentException(
453
                    'progress client option must be callable'
454
                );
455
            }
456
            $conf[CURLOPT_NOPROGRESS] = false;
457
            $conf[CURLOPT_PROGRESSFUNCTION] = function () use ($progress) {
458
                $args = func_get_args();
459
                // PHP 5.5 pushed the handle onto the start of the args
460
                if (is_resource($args[0])) {
461
                    array_shift($args);
462
                }
463
                call_user_func_array($progress, $args);
464
            };
465
        }
466
467
        if (!empty($options['debug'])) {
468
            $conf[CURLOPT_STDERR] = \GuzzleHttp\debug_resource($options['debug']);
469
            $conf[CURLOPT_VERBOSE] = true;
470
        }
471
    }
472
473
    /**
474
     * This function ensures that a response was set on a transaction. If one
475
     * was not set, then the request is retried if possible. This error
476
     * typically means you are sending a payload, curl encountered a
477
     * "Connection died, retrying a fresh connect" error, tried to rewind the
478
     * stream, and then encountered a "necessary data rewind wasn't possible"
479
     * error, causing the request to be sent through curl_multi_info_read()
480
     * without an error status.
481
     */
482
    private static function retryFailedRewind(
483
        callable $handler,
484
        EasyHandle $easy,
485
        array $ctx
486
    ) {
487
        try {
488
            // Only rewind if the body has been read from.
489
            $body = $easy->request->getBody();
490
            if ($body->tell() > 0) {
491
                $body->rewind();
492
            }
493
        } catch (\RuntimeException $e) {
494
            $ctx['error'] = 'The connection unexpectedly failed without '
495
                . 'providing an error. The request would have been retried, '
496
                . 'but attempting to rewind the request body failed. '
497
                . 'Exception: ' . $e;
498
            return self::createRejection($easy, $ctx);
499
        }
500
501
        // Retry no more than 3 times before giving up.
502
        if (!isset($easy->options['_curl_retries'])) {
503
            $easy->options['_curl_retries'] = 1;
504
        } elseif ($easy->options['_curl_retries'] == 2) {
505
            $ctx['error'] = 'The cURL request was retried 3 times '
506
                . 'and did not succeed. The most likely reason for the failure '
507
                . 'is that cURL was unable to rewind the body of the request '
508
                . 'and subsequent retries resulted in the same error. Turn on '
509
                . 'the debug option to see what went wrong. See '
510
                . 'https://bugs.php.net/bug.php?id=47204 for more information.';
511
            return self::createRejection($easy, $ctx);
512
        } else {
513
            $easy->options['_curl_retries']++;
514
        }
515
516
        return $handler($easy->request, $easy->options);
517
    }
518
519
    private function createHeaderFn(EasyHandle $easy)
520
    {
521
        if (isset($easy->options['on_headers'])) {
522
            $onHeaders = $easy->options['on_headers'];
523
524
            if (!is_callable($onHeaders)) {
525
                throw new \InvalidArgumentException('on_headers must be callable');
526
            }
527
        } else {
528
            $onHeaders = null;
529
        }
530
531
        return function ($ch, $h) use (
532
            $onHeaders,
533
            $easy,
534
            &$startingResponse
535
        ) {
536
            $value = trim($h);
537
            if ($value === '') {
538
                $startingResponse = true;
539
                $easy->createResponse();
540
                if ($onHeaders !== null) {
541
                    try {
542
                        $onHeaders($easy->response);
543
                    } catch (\Exception $e) {
544
                        // Associate the exception with the handle and trigger
545
                        // a curl header write error by returning 0.
546
                        $easy->onHeadersException = $e;
547
                        return -1;
548
                    }
549
                }
550
            } elseif ($startingResponse) {
551
                $startingResponse = false;
552
                $easy->headers = [$value];
553
            } else {
554
                $easy->headers[] = $value;
555
            }
556
            return strlen($h);
557
        };
558
    }
559
}
560