Issues (56)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Handler/CurlFactory.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
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)
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'];
472
                } else {
473
                    [$sslKey] = $options['ssl_key'];
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