PluginProbe
Plausible Analytics / trunk
Plausible Analytics vtrunk
2.6.1 2.6.0 trunk 1.0.0 1.0.1 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 All 52 releases
plausible-analytics / src / Client / lib / Lib / GuzzleHttp / Handler / CurlFactory.php

CurlFactory.php in Plausible Analytics trunk, at src/Client/lib/Lib/GuzzleHttp/Handler/CurlFactory.php

639 lines 24.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Plausible\Analytics\WP\Client\Lib\GuzzleHttp\Handler;
4
5 use Plausible\Analytics\WP\Client\Lib\GuzzleHttp\Exception\ConnectException;
6 use Plausible\Analytics\WP\Client\Lib\GuzzleHttp\Exception\RequestException;
7 use Plausible\Analytics\WP\Client\Lib\GuzzleHttp\Promise as P;
8 use Plausible\Analytics\WP\Client\Lib\GuzzleHttp\Promise\FulfilledPromise;
9 use Plausible\Analytics\WP\Client\Lib\GuzzleHttp\Promise\PromiseInterface;
10 use Plausible\Analytics\WP\Client\Lib\GuzzleHttp\Psr7\LazyOpenStream;
11 use Plausible\Analytics\WP\Client\Lib\GuzzleHttp\TransferStats;
12 use Plausible\Analytics\WP\Client\Lib\GuzzleHttp\Utils;
13 use Plausible\Analytics\WP\Client\Lib\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>
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 => 300,
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
254 return;
255 }
256
257 $method = $easy->request->getMethod();
258 if ($method === 'PUT' || $method === 'POST') {
259 // See https://tools.ietf.org/html/rfc7230#section-3.3.2
260 if (!$easy->request->hasHeader('Content-Length')) {
261 $conf[\CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
262 }
263 } elseif ($method === 'HEAD') {
264 $conf[\CURLOPT_NOBODY] = true;
265 unset(
266 $conf[\CURLOPT_WRITEFUNCTION],
267 $conf[\CURLOPT_READFUNCTION],
268 $conf[\CURLOPT_FILE],
269 $conf[\CURLOPT_INFILE]
270 );
271 }
272 }
273
274 private function applyBody(RequestInterface $request, array $options, array &$conf): void
275 {
276 $size = $request->hasHeader('Content-Length')
277 ? (int) $request->getHeaderLine('Content-Length')
278 : null;
279
280 // Send the body as a string if the size is less than 1MB OR if the
281 // [curl][body_as_string] request value is set.
282 if (($size !== null && $size < 1000000) || !empty($options['_body_as_string'])) {
283 $conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody();
284 // Don't duplicate the Content-Length header
285 $this->removeHeader('Content-Length', $conf);
286 $this->removeHeader('Transfer-Encoding', $conf);
287 } else {
288 $conf[\CURLOPT_UPLOAD] = true;
289 if ($size !== null) {
290 $conf[\CURLOPT_INFILESIZE] = $size;
291 $this->removeHeader('Content-Length', $conf);
292 }
293 $body = $request->getBody();
294 if ($body->isSeekable()) {
295 $body->rewind();
296 }
297 $conf[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body) {
298 return $body->read($length);
299 };
300 }
301
302 // If the Expect header is not present, prevent curl from adding it
303 if (!$request->hasHeader('Expect')) {
304 $conf[\CURLOPT_HTTPHEADER][] = 'Expect:';
305 }
306
307 // cURL sometimes adds a content-type by default. Prevent this.
308 if (!$request->hasHeader('Content-Type')) {
309 $conf[\CURLOPT_HTTPHEADER][] = 'Content-Type:';
310 }
311 }
312
313 private function applyHeaders(EasyHandle $easy, array &$conf): void
314 {
315 foreach ($conf['_headers'] as $name => $values) {
316 foreach ($values as $value) {
317 $value = (string) $value;
318 if ($value === '') {
319 // cURL requires a special format for empty headers.
320 // See https://github.com/guzzle/guzzle/issues/1882 for more details.
321 $conf[\CURLOPT_HTTPHEADER][] = "$name;";
322 } else {
323 $conf[\CURLOPT_HTTPHEADER][] = "$name: $value";
324 }
325 }
326 }
327
328 // Remove the Accept header if one was not set
329 if (!$easy->request->hasHeader('Accept')) {
330 $conf[\CURLOPT_HTTPHEADER][] = 'Accept:';
331 }
332 }
333
334 /**
335 * Remove a header from the options array.
336 *
337 * @param string $name Case-insensitive header to remove
338 * @param array $options Array of options to modify
339 */
340 private function removeHeader(string $name, array &$options): void
341 {
342 foreach (\array_keys($options['_headers']) as $key) {
343 if (!\strcasecmp($key, $name)) {
344 unset($options['_headers'][$key]);
345
346 return;
347 }
348 }
349 }
350
351 private function applyHandlerOptions(EasyHandle $easy, array &$conf): void
352 {
353 $options = $easy->options;
354 if (isset($options['verify'])) {
355 if ($options['verify'] === false) {
356 unset($conf[\CURLOPT_CAINFO]);
357 $conf[\CURLOPT_SSL_VERIFYHOST] = 0;
358 $conf[\CURLOPT_SSL_VERIFYPEER] = false;
359 } else {
360 $conf[\CURLOPT_SSL_VERIFYHOST] = 2;
361 $conf[\CURLOPT_SSL_VERIFYPEER] = true;
362 if (\is_string($options['verify'])) {
363 // Throw an error if the file/folder/link path is not valid or doesn't exist.
364 if (!\file_exists($options['verify'])) {
365 throw new \InvalidArgumentException("SSL CA bundle not found: {$options['verify']}");
366 }
367 // If it's a directory or a link to a directory use CURLOPT_CAPATH.
368 // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO.
369 if (
370 \is_dir($options['verify'])
371 || (
372 \is_link($options['verify']) === true
373 && ($verifyLink = \readlink($options['verify'])) !== false
374 && \is_dir($verifyLink)
375 )
376 ) {
377 $conf[\CURLOPT_CAPATH] = $options['verify'];
378 } else {
379 $conf[\CURLOPT_CAINFO] = $options['verify'];
380 }
381 }
382 }
383 }
384
385 if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) {
386 $accept = $easy->request->getHeaderLine('Accept-Encoding');
387 if ($accept) {
388 $conf[\CURLOPT_ENCODING] = $accept;
389 } else {
390 // The empty string enables all available decoders and implicitly
391 // sets a matching 'Accept-Encoding' header.
392 $conf[\CURLOPT_ENCODING] = '';
393 // But as the user did not specify any acceptable encodings we need
394 // to overwrite this implicit header with an empty one.
395 $conf[\CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
396 }
397 }
398
399 if (!isset($options['sink'])) {
400 // Use a default temp stream if no sink was set.
401 $options['sink'] = \Plausible\Analytics\WP\Client\Lib\GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'w+');
402 }
403 $sink = $options['sink'];
404 if (!\is_string($sink)) {
405 $sink = \Plausible\Analytics\WP\Client\Lib\GuzzleHttp\Psr7\Utils::streamFor($sink);
406 } elseif (!\is_dir(\dirname($sink))) {
407 // Ensure that the directory exists before failing in curl.
408 throw new \RuntimeException(\sprintf('Directory %s does not exist for sink value of %s', \dirname($sink), $sink));
409 } else {
410 $sink = new LazyOpenStream($sink, 'w+');
411 }
412 $easy->sink = $sink;
413 $conf[\CURLOPT_WRITEFUNCTION] = static function ($ch, $write) use ($sink): int {
414 return $sink->write($write);
415 };
416
417 $timeoutRequiresNoSignal = false;
418 if (isset($options['timeout'])) {
419 $timeoutRequiresNoSignal |= $options['timeout'] < 1;
420 $conf[\CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
421 }
422
423 // CURL default value is CURL_IPRESOLVE_WHATEVER
424 if (isset($options['force_ip_resolve'])) {
425 if ('v4' === $options['force_ip_resolve']) {
426 $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4;
427 } elseif ('v6' === $options['force_ip_resolve']) {
428 $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V6;
429 }
430 }
431
432 if (isset($options['connect_timeout'])) {
433 $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1;
434 $conf[\CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
435 }
436
437 if ($timeoutRequiresNoSignal && \strtoupper(\substr(\PHP_OS, 0, 3)) !== 'WIN') {
438 $conf[\CURLOPT_NOSIGNAL] = true;
439 }
440
441 if (isset($options['proxy'])) {
442 if (!\is_array($options['proxy'])) {
443 $conf[\CURLOPT_PROXY] = $options['proxy'];
444 } else {
445 $scheme = $easy->request->getUri()->getScheme();
446 if (isset($options['proxy'][$scheme])) {
447 $host = $easy->request->getUri()->getHost();
448 if (isset($options['proxy']['no']) && Utils::isHostInNoProxy($host, $options['proxy']['no'])) {
449 unset($conf[\CURLOPT_PROXY]);
450 } else {
451 $conf[\CURLOPT_PROXY] = $options['proxy'][$scheme];
452 }
453 }
454 }
455 }
456
457 if (isset($options['crypto_method'])) {
458 if (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']) {
459 if (!defined('CURL_SSLVERSION_TLSv1_0')) {
460 throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.0 not supported by your version of cURL');
461 }
462 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_0;
463 } elseif (\STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method']) {
464 if (!defined('CURL_SSLVERSION_TLSv1_1')) {
465 throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.1 not supported by your version of cURL');
466 }
467 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_1;
468 } elseif (\STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) {
469 if (!defined('CURL_SSLVERSION_TLSv1_2')) {
470 throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.2 not supported by your version of cURL');
471 }
472 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2;
473 } elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) {
474 if (!defined('CURL_SSLVERSION_TLSv1_3')) {
475 throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL');
476 }
477 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3;
478 } else {
479 throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
480 }
481 }
482
483 if (isset($options['cert'])) {
484 $cert = $options['cert'];
485 if (\is_array($cert)) {
486 $conf[\CURLOPT_SSLCERTPASSWD] = $cert[1];
487 $cert = $cert[0];
488 }
489 if (!\file_exists($cert)) {
490 throw new \InvalidArgumentException("SSL certificate not found: {$cert}");
491 }
492 // OpenSSL (versions 0.9.3 and later) also support "P12" for PKCS#12-encoded files.
493 // see https://curl.se/libcurl/c/CURLOPT_SSLCERTTYPE.html
494 $ext = pathinfo($cert, \PATHINFO_EXTENSION);
495 if (preg_match('#^(der|p12)$#i', $ext)) {
496 $conf[\CURLOPT_SSLCERTTYPE] = strtoupper($ext);
497 }
498 $conf[\CURLOPT_SSLCERT] = $cert;
499 }
500
501 if (isset($options['ssl_key'])) {
502 if (\is_array($options['ssl_key'])) {
503 if (\count($options['ssl_key']) === 2) {
504 [$sslKey, $conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key'];
505 } else {
506 [$sslKey] = $options['ssl_key'];
507 }
508 }
509
510 $sslKey = $sslKey ?? $options['ssl_key'];
511
512 if (!\file_exists($sslKey)) {
513 throw new \InvalidArgumentException("SSL private key not found: {$sslKey}");
514 }
515 $conf[\CURLOPT_SSLKEY] = $sslKey;
516 }
517
518 if (isset($options['progress'])) {
519 $progress = $options['progress'];
520 if (!\is_callable($progress)) {
521 throw new \InvalidArgumentException('progress client option must be callable');
522 }
523 $conf[\CURLOPT_NOPROGRESS] = false;
524 $conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use ($progress) {
525 $progress($downloadSize, $downloaded, $uploadSize, $uploaded);
526 };
527 }
528
529 if (!empty($options['debug'])) {
530 $conf[\CURLOPT_STDERR] = Utils::debugResource($options['debug']);
531 $conf[\CURLOPT_VERBOSE] = true;
532 }
533 }
534
535 /**
536 * This function ensures that a response was set on a transaction. If one
537 * was not set, then the request is retried if possible. This error
538 * typically means you are sending a payload, curl encountered a
539 * "Connection died, retrying a fresh connect" error, tried to rewind the
540 * stream, and then encountered a "necessary data rewind wasn't possible"
541 * error, causing the request to be sent through curl_multi_info_read()
542 * without an error status.
543 *
544 * @param callable(RequestInterface, array): PromiseInterface $handler
545 */
546 private static function retryFailedRewind(callable $handler, EasyHandle $easy, array $ctx): PromiseInterface
547 {
548 try {
549 // Only rewind if the body has been read from.
550 $body = $easy->request->getBody();
551 if ($body->tell() > 0) {
552 $body->rewind();
553 }
554 } catch (\RuntimeException $e) {
555 $ctx['error'] = 'The connection unexpectedly failed without '
556 .'providing an error. The request would have been retried, '
557 .'but attempting to rewind the request body failed. '
558 .'Exception: '.$e;
559
560 return self::createRejection($easy, $ctx);
561 }
562
563 // Retry no more than 3 times before giving up.
564 if (!isset($easy->options['_curl_retries'])) {
565 $easy->options['_curl_retries'] = 1;
566 } elseif ($easy->options['_curl_retries'] == 2) {
567 $ctx['error'] = 'The cURL request was retried 3 times '
568 .'and did not succeed. The most likely reason for the failure '
569 .'is that cURL was unable to rewind the body of the request '
570 .'and subsequent retries resulted in the same error. Turn on '
571 .'the debug option to see what went wrong. See '
572 .'https://bugs.php.net/bug.php?id=47204 for more information.';
573
574 return self::createRejection($easy, $ctx);
575 } else {
576 ++$easy->options['_curl_retries'];
577 }
578
579 return $handler($easy->request, $easy->options);
580 }
581
582 private function createHeaderFn(EasyHandle $easy): callable
583 {
584 if (isset($easy->options['on_headers'])) {
585 $onHeaders = $easy->options['on_headers'];
586
587 if (!\is_callable($onHeaders)) {
588 throw new \InvalidArgumentException('on_headers must be callable');
589 }
590 } else {
591 $onHeaders = null;
592 }
593
594 return static function ($ch, $h) use (
595 $onHeaders,
596 $easy,
597 &$startingResponse
598 ) {
599 $value = \trim($h);
600 if ($value === '') {
601 $startingResponse = true;
602 try {
603 $easy->createResponse();
604 } catch (\Exception $e) {
605 $easy->createResponseException = $e;
606
607 return -1;
608 }
609 if ($onHeaders !== null) {
610 try {
611 $onHeaders($easy->response);
612 } catch (\Exception $e) {
613 // Associate the exception with the handle and trigger
614 // a curl header write error by returning 0.
615 $easy->onHeadersException = $e;
616
617 return -1;
618 }
619 }
620 } elseif ($startingResponse) {
621 $startingResponse = false;
622 $easy->headers = [$value];
623 } else {
624 $easy->headers[] = $value;
625 }
626
627 return \strlen($h);
628 };
629 }
630
631 public function __destruct()
632 {
633 foreach ($this->handles as $id => $handle) {
634 \curl_close($handle);
635 unset($this->handles[$id]);
636 }
637 }
638 }
639