source: vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php

Last change on this file was e3d4e0a, checked in by Vlado 222039 <vlado.popovski@…>, 7 days ago

Upload project files

  • Property mode set to 100644
File size: 27.5 KB
Line 
1<?php
2
3namespace GuzzleHttp\Handler;
4
5use GuzzleHttp\Exception\ConnectException;
6use GuzzleHttp\Exception\RequestException;
7use GuzzleHttp\Promise as P;
8use GuzzleHttp\Promise\FulfilledPromise;
9use GuzzleHttp\Promise\PromiseInterface;
10use GuzzleHttp\Psr7\LazyOpenStream;
11use GuzzleHttp\TransferStats;
12use GuzzleHttp\Utils;
13use Psr\Http\Message\RequestInterface;
14use Psr\Http\Message\UriInterface;
15
16/**
17 * Creates curl resources from a request
18 *
19 * @final
20 */
21class CurlFactory implements CurlFactoryInterface
22{
23 public const CURL_VERSION_STR = 'curl_version';
24
25 /**
26 * @deprecated
27 */
28 public const LOW_CURL_VERSION_NUMBER = '7.21.2';
29
30 /**
31 * @var resource[]|\CurlHandle[]
32 */
33 private $handles = [];
34
35 /**
36 * @var int Total number of idle handles to keep in cache
37 */
38 private $maxHandles;
39
40 /**
41 * @param int $maxHandles Maximum number of idle handles.
42 */
43 public function __construct(int $maxHandles)
44 {
45 $this->maxHandles = $maxHandles;
46 }
47
48 public function create(RequestInterface $request, array $options): EasyHandle
49 {
50 $protocolVersion = $request->getProtocolVersion();
51
52 if ('2' === $protocolVersion || '2.0' === $protocolVersion) {
53 if (!self::supportsHttp2()) {
54 throw new ConnectException('HTTP/2 is supported by the cURL handler, however libcurl is built without HTTP/2 support.', $request);
55 }
56 } elseif ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) {
57 throw new ConnectException(sprintf('HTTP/%s is not supported by the cURL handler.', $protocolVersion), $request);
58 }
59
60 if (isset($options['curl']['body_as_string'])) {
61 $options['_body_as_string'] = $options['curl']['body_as_string'];
62 unset($options['curl']['body_as_string']);
63 }
64
65 $easy = new EasyHandle();
66 $easy->request = $request;
67 $easy->options = $options;
68 $conf = $this->getDefaultConf($easy);
69 $this->applyMethod($easy, $conf);
70 $this->applyHandlerOptions($easy, $conf);
71 $this->applyHeaders($easy, $conf);
72 unset($conf['_headers']);
73
74 // Add handler options from the request configuration options
75 if (isset($options['curl'])) {
76 $conf = \array_replace($conf, $options['curl']);
77 }
78
79 $conf[\CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
80 $easy->handle = $this->handles ? \array_pop($this->handles) : \curl_init();
81 curl_setopt_array($easy->handle, $conf);
82
83 return $easy;
84 }
85
86 private static function supportsHttp2(): bool
87 {
88 static $supportsHttp2 = null;
89
90 if (null === $supportsHttp2) {
91 $supportsHttp2 = self::supportsTls12()
92 && defined('CURL_VERSION_HTTP2')
93 && (\CURL_VERSION_HTTP2 & \curl_version()['features']);
94 }
95
96 return $supportsHttp2;
97 }
98
99 private static function supportsTls12(): bool
100 {
101 static $supportsTls12 = null;
102
103 if (null === $supportsTls12) {
104 $supportsTls12 = \CURL_SSLVERSION_TLSv1_2 & \curl_version()['features'];
105 }
106
107 return $supportsTls12;
108 }
109
110 private static function supportsTls13(): bool
111 {
112 static $supportsTls13 = null;
113
114 if (null === $supportsTls13) {
115 $supportsTls13 = defined('CURL_SSLVERSION_TLSv1_3')
116 && (\CURL_SSLVERSION_TLSv1_3 & \curl_version()['features']);
117 }
118
119 return $supportsTls13;
120 }
121
122 public function release(EasyHandle $easy): void
123 {
124 $resource = $easy->handle;
125 unset($easy->handle);
126
127 if (\count($this->handles) >= $this->maxHandles) {
128 \curl_close($resource);
129 } else {
130 // Remove all callback functions as they can hold onto references
131 // and are not cleaned up by curl_reset. Using curl_setopt_array
132 // does not work for some reason, so removing each one
133 // individually.
134 \curl_setopt($resource, \CURLOPT_HEADERFUNCTION, null);
135 \curl_setopt($resource, \CURLOPT_READFUNCTION, null);
136 \curl_setopt($resource, \CURLOPT_WRITEFUNCTION, null);
137 \curl_setopt($resource, \CURLOPT_PROGRESSFUNCTION, null);
138 \curl_reset($resource);
139 $this->handles[] = $resource;
140 }
141 }
142
143 /**
144 * Completes a cURL transaction, either returning a response promise or a
145 * rejected promise.
146 *
147 * @param callable(RequestInterface, array): PromiseInterface $handler
148 * @param CurlFactoryInterface $factory Dictates how the handle is released
149 */
150 public static function finish(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface
151 {
152 if (isset($easy->options['on_stats'])) {
153 self::invokeStats($easy);
154 }
155
156 if (!$easy->response || $easy->errno) {
157 return self::finishError($handler, $easy, $factory);
158 }
159
160 // Return the response if it is present and there is no error.
161 $factory->release($easy);
162
163 // Rewind the body of the response if possible.
164 $body = $easy->response->getBody();
165 if ($body->isSeekable()) {
166 $body->rewind();
167 }
168
169 return new FulfilledPromise($easy->response);
170 }
171
172 private static function invokeStats(EasyHandle $easy): void
173 {
174 $curlStats = \curl_getinfo($easy->handle);
175 $curlStats['appconnect_time'] = \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME);
176 $stats = new TransferStats(
177 $easy->request,
178 $easy->response,
179 $curlStats['total_time'],
180 $easy->errno,
181 $curlStats
182 );
183 ($easy->options['on_stats'])($stats);
184 }
185
186 /**
187 * @param callable(RequestInterface, array): PromiseInterface $handler
188 */
189 private static function finishError(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface
190 {
191 // Get error information and release the handle to the factory.
192 $ctx = [
193 'errno' => $easy->errno,
194 'error' => \curl_error($easy->handle),
195 'appconnect_time' => \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME),
196 ] + \curl_getinfo($easy->handle);
197 $ctx[self::CURL_VERSION_STR] = self::getCurlVersion();
198 $factory->release($easy);
199
200 // Retry when nothing is present or when curl failed to rewind.
201 if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65)) {
202 return self::retryFailedRewind($handler, $easy, $ctx);
203 }
204
205 return self::createRejection($easy, $ctx);
206 }
207
208 private static function getCurlVersion(): string
209 {
210 static $curlVersion = null;
211
212 if (null === $curlVersion) {
213 $curlVersion = \curl_version()['version'];
214 }
215
216 return $curlVersion;
217 }
218
219 private static function createRejection(EasyHandle $easy, array $ctx): PromiseInterface
220 {
221 static $connectionErrors = [
222 \CURLE_OPERATION_TIMEOUTED => true,
223 \CURLE_COULDNT_RESOLVE_HOST => true,
224 \CURLE_COULDNT_CONNECT => true,
225 \CURLE_SSL_CONNECT_ERROR => true,
226 \CURLE_GOT_NOTHING => true,
227 ];
228
229 if ($easy->createResponseException) {
230 return P\Create::rejectionFor(
231 new RequestException(
232 'An error was encountered while creating the response',
233 $easy->request,
234 $easy->response,
235 $easy->createResponseException,
236 $ctx
237 )
238 );
239 }
240
241 // If an exception was encountered during the onHeaders event, then
242 // return a rejected promise that wraps that exception.
243 if ($easy->onHeadersException) {
244 return P\Create::rejectionFor(
245 new RequestException(
246 'An error was encountered during the on_headers event',
247 $easy->request,
248 $easy->response,
249 $easy->onHeadersException,
250 $ctx
251 )
252 );
253 }
254
255 $uri = $easy->request->getUri();
256
257 $sanitizedError = self::sanitizeCurlError($ctx['error'] ?? '', $uri);
258
259 $message = \sprintf(
260 'cURL error %s: %s (%s)',
261 $ctx['errno'],
262 $sanitizedError,
263 'see https://curl.haxx.se/libcurl/c/libcurl-errors.html'
264 );
265
266 if ('' !== $sanitizedError) {
267 $redactedUriString = \GuzzleHttp\Psr7\Utils::redactUserInfo($uri)->__toString();
268 if ($redactedUriString !== '' && false === \strpos($sanitizedError, $redactedUriString)) {
269 $message .= \sprintf(' for %s', $redactedUriString);
270 }
271 }
272
273 // Create a connection exception if it was a specific error code.
274 $error = isset($connectionErrors[$easy->errno])
275 ? new ConnectException($message, $easy->request, null, $ctx)
276 : new RequestException($message, $easy->request, $easy->response, null, $ctx);
277
278 return P\Create::rejectionFor($error);
279 }
280
281 private static function sanitizeCurlError(string $error, UriInterface $uri): string
282 {
283 if ('' === $error) {
284 return $error;
285 }
286
287 $baseUri = $uri->withQuery('')->withFragment('');
288 $baseUriString = $baseUri->__toString();
289
290 if ('' === $baseUriString) {
291 return $error;
292 }
293
294 $redactedUriString = \GuzzleHttp\Psr7\Utils::redactUserInfo($baseUri)->__toString();
295
296 return str_replace($baseUriString, $redactedUriString, $error);
297 }
298
299 /**
300 * @return array<int|string, mixed>
301 */
302 private function getDefaultConf(EasyHandle $easy): array
303 {
304 $conf = [
305 '_headers' => $easy->request->getHeaders(),
306 \CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(),
307 \CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''),
308 \CURLOPT_RETURNTRANSFER => false,
309 \CURLOPT_HEADER => false,
310 \CURLOPT_CONNECTTIMEOUT => 300,
311 ];
312
313 if (\defined('CURLOPT_PROTOCOLS')) {
314 $conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP | \CURLPROTO_HTTPS;
315 }
316
317 $version = $easy->request->getProtocolVersion();
318
319 if ('2' === $version || '2.0' === $version) {
320 $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
321 } elseif ('1.1' === $version) {
322 $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
323 } else {
324 $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
325 }
326
327 return $conf;
328 }
329
330 private function applyMethod(EasyHandle $easy, array &$conf): void
331 {
332 $body = $easy->request->getBody();
333 $size = $body->getSize();
334
335 if ($size === null || $size > 0) {
336 $this->applyBody($easy->request, $easy->options, $conf);
337
338 return;
339 }
340
341 $method = $easy->request->getMethod();
342 if ($method === 'PUT' || $method === 'POST') {
343 // See https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
344 if (!$easy->request->hasHeader('Content-Length')) {
345 $conf[\CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
346 }
347 } elseif ($method === 'HEAD') {
348 $conf[\CURLOPT_NOBODY] = true;
349 unset(
350 $conf[\CURLOPT_WRITEFUNCTION],
351 $conf[\CURLOPT_READFUNCTION],
352 $conf[\CURLOPT_FILE],
353 $conf[\CURLOPT_INFILE]
354 );
355 }
356 }
357
358 private function applyBody(RequestInterface $request, array $options, array &$conf): void
359 {
360 $size = $request->hasHeader('Content-Length')
361 ? (int) $request->getHeaderLine('Content-Length')
362 : null;
363
364 // Send the body as a string if the size is less than 1MB OR if the
365 // [curl][body_as_string] request value is set.
366 if (($size !== null && $size < 1000000) || !empty($options['_body_as_string'])) {
367 $conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody();
368 // Don't duplicate the Content-Length header
369 $this->removeHeader('Content-Length', $conf);
370 $this->removeHeader('Transfer-Encoding', $conf);
371 } else {
372 $conf[\CURLOPT_UPLOAD] = true;
373 if ($size !== null) {
374 $conf[\CURLOPT_INFILESIZE] = $size;
375 $this->removeHeader('Content-Length', $conf);
376 }
377 $body = $request->getBody();
378 if ($body->isSeekable()) {
379 $body->rewind();
380 }
381 $conf[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body) {
382 return $body->read($length);
383 };
384 }
385
386 // If the Expect header is not present, prevent curl from adding it
387 if (!$request->hasHeader('Expect')) {
388 $conf[\CURLOPT_HTTPHEADER][] = 'Expect:';
389 }
390
391 // cURL sometimes adds a content-type by default. Prevent this.
392 if (!$request->hasHeader('Content-Type')) {
393 $conf[\CURLOPT_HTTPHEADER][] = 'Content-Type:';
394 }
395 }
396
397 private function applyHeaders(EasyHandle $easy, array &$conf): void
398 {
399 foreach ($conf['_headers'] as $name => $values) {
400 foreach ($values as $value) {
401 $value = (string) $value;
402 if ($value === '') {
403 // cURL requires a special format for empty headers.
404 // See https://github.com/guzzle/guzzle/issues/1882 for more details.
405 $conf[\CURLOPT_HTTPHEADER][] = "$name;";
406 } else {
407 $conf[\CURLOPT_HTTPHEADER][] = "$name: $value";
408 }
409 }
410 }
411
412 // Remove the Accept header if one was not set
413 if (!$easy->request->hasHeader('Accept')) {
414 $conf[\CURLOPT_HTTPHEADER][] = 'Accept:';
415 }
416 }
417
418 /**
419 * Remove a header from the options array.
420 *
421 * @param string $name Case-insensitive header to remove
422 * @param array $options Array of options to modify
423 */
424 private function removeHeader(string $name, array &$options): void
425 {
426 foreach (\array_keys($options['_headers']) as $key) {
427 if (!\strcasecmp($key, $name)) {
428 unset($options['_headers'][$key]);
429
430 return;
431 }
432 }
433 }
434
435 private function applyHandlerOptions(EasyHandle $easy, array &$conf): void
436 {
437 $options = $easy->options;
438 if (isset($options['verify'])) {
439 if ($options['verify'] === false) {
440 unset($conf[\CURLOPT_CAINFO]);
441 $conf[\CURLOPT_SSL_VERIFYHOST] = 0;
442 $conf[\CURLOPT_SSL_VERIFYPEER] = false;
443 } else {
444 $conf[\CURLOPT_SSL_VERIFYHOST] = 2;
445 $conf[\CURLOPT_SSL_VERIFYPEER] = true;
446 if (\is_string($options['verify'])) {
447 // Throw an error if the file/folder/link path is not valid or doesn't exist.
448 if (!\file_exists($options['verify'])) {
449 throw new \InvalidArgumentException("SSL CA bundle not found: {$options['verify']}");
450 }
451 // If it's a directory or a link to a directory use CURLOPT_CAPATH.
452 // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO.
453 if (
454 \is_dir($options['verify'])
455 || (
456 \is_link($options['verify']) === true
457 && ($verifyLink = \readlink($options['verify'])) !== false
458 && \is_dir($verifyLink)
459 )
460 ) {
461 $conf[\CURLOPT_CAPATH] = $options['verify'];
462 } else {
463 $conf[\CURLOPT_CAINFO] = $options['verify'];
464 }
465 }
466 }
467 }
468
469 if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) {
470 $accept = $easy->request->getHeaderLine('Accept-Encoding');
471 if ($accept) {
472 $conf[\CURLOPT_ENCODING] = $accept;
473 } else {
474 // The empty string enables all available decoders and implicitly
475 // sets a matching 'Accept-Encoding' header.
476 $conf[\CURLOPT_ENCODING] = '';
477 // But as the user did not specify any encoding preference,
478 // let's leave it up to server by preventing curl from sending
479 // the header, which will be interpreted as 'Accept-Encoding: *'.
480 // https://www.rfc-editor.org/rfc/rfc9110#field.accept-encoding
481 $conf[\CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
482 }
483 }
484
485 if (!isset($options['sink'])) {
486 // Use a default temp stream if no sink was set.
487 $options['sink'] = \GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'w+');
488 }
489 $sink = $options['sink'];
490 if (!\is_string($sink)) {
491 $sink = \GuzzleHttp\Psr7\Utils::streamFor($sink);
492 } elseif (!\is_dir(\dirname($sink))) {
493 // Ensure that the directory exists before failing in curl.
494 throw new \RuntimeException(\sprintf('Directory %s does not exist for sink value of %s', \dirname($sink), $sink));
495 } else {
496 $sink = new LazyOpenStream($sink, 'w+');
497 }
498 $easy->sink = $sink;
499 $conf[\CURLOPT_WRITEFUNCTION] = static function ($ch, $write) use ($sink): int {
500 return $sink->write($write);
501 };
502
503 $timeoutRequiresNoSignal = false;
504 if (isset($options['timeout'])) {
505 $timeoutRequiresNoSignal |= $options['timeout'] < 1;
506 $conf[\CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
507 }
508
509 // CURL default value is CURL_IPRESOLVE_WHATEVER
510 if (isset($options['force_ip_resolve'])) {
511 if ('v4' === $options['force_ip_resolve']) {
512 $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4;
513 } elseif ('v6' === $options['force_ip_resolve']) {
514 $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V6;
515 }
516 }
517
518 if (isset($options['connect_timeout'])) {
519 $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1;
520 $conf[\CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
521 }
522
523 if ($timeoutRequiresNoSignal && \strtoupper(\substr(\PHP_OS, 0, 3)) !== 'WIN') {
524 $conf[\CURLOPT_NOSIGNAL] = true;
525 }
526
527 if (isset($options['proxy'])) {
528 if (!\is_array($options['proxy'])) {
529 $conf[\CURLOPT_PROXY] = $options['proxy'];
530 } else {
531 $scheme = $easy->request->getUri()->getScheme();
532 if (isset($options['proxy'][$scheme])) {
533 $host = $easy->request->getUri()->getHost();
534 if (isset($options['proxy']['no']) && Utils::isHostInNoProxy($host, $options['proxy']['no'])) {
535 unset($conf[\CURLOPT_PROXY]);
536 } else {
537 $conf[\CURLOPT_PROXY] = $options['proxy'][$scheme];
538 }
539 }
540 }
541 }
542
543 if (isset($options['crypto_method'])) {
544 $protocolVersion = $easy->request->getProtocolVersion();
545
546 // If HTTP/2, upgrade TLS 1.0 and 1.1 to 1.2
547 if ('2' === $protocolVersion || '2.0' === $protocolVersion) {
548 if (
549 \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']
550 || \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method']
551 || \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']
552 ) {
553 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2;
554 } elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) {
555 if (!self::supportsTls13()) {
556 throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL');
557 }
558 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3;
559 } else {
560 throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
561 }
562 } elseif (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']) {
563 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_0;
564 } elseif (\STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method']) {
565 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_1;
566 } elseif (\STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) {
567 if (!self::supportsTls12()) {
568 throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.2 not supported by your version of cURL');
569 }
570 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2;
571 } elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) {
572 if (!self::supportsTls13()) {
573 throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL');
574 }
575 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3;
576 } else {
577 throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
578 }
579 }
580
581 if (isset($options['cert'])) {
582 $cert = $options['cert'];
583 if (\is_array($cert)) {
584 $conf[\CURLOPT_SSLCERTPASSWD] = $cert[1];
585 $cert = $cert[0];
586 }
587 if (!\file_exists($cert)) {
588 throw new \InvalidArgumentException("SSL certificate not found: {$cert}");
589 }
590 // OpenSSL (versions 0.9.3 and later) also support "P12" for PKCS#12-encoded files.
591 // see https://curl.se/libcurl/c/CURLOPT_SSLCERTTYPE.html
592 $ext = pathinfo($cert, \PATHINFO_EXTENSION);
593 if (preg_match('#^(der|p12)$#i', $ext)) {
594 $conf[\CURLOPT_SSLCERTTYPE] = strtoupper($ext);
595 }
596 $conf[\CURLOPT_SSLCERT] = $cert;
597 }
598
599 if (isset($options['ssl_key'])) {
600 if (\is_array($options['ssl_key'])) {
601 if (\count($options['ssl_key']) === 2) {
602 [$sslKey, $conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key'];
603 } else {
604 [$sslKey] = $options['ssl_key'];
605 }
606 }
607
608 $sslKey = $sslKey ?? $options['ssl_key'];
609
610 if (!\file_exists($sslKey)) {
611 throw new \InvalidArgumentException("SSL private key not found: {$sslKey}");
612 }
613 $conf[\CURLOPT_SSLKEY] = $sslKey;
614 }
615
616 if (isset($options['progress'])) {
617 $progress = $options['progress'];
618 if (!\is_callable($progress)) {
619 throw new \InvalidArgumentException('progress client option must be callable');
620 }
621 $conf[\CURLOPT_NOPROGRESS] = false;
622 $conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use ($progress) {
623 $progress($downloadSize, $downloaded, $uploadSize, $uploaded);
624 };
625 }
626
627 if (!empty($options['debug'])) {
628 $conf[\CURLOPT_STDERR] = Utils::debugResource($options['debug']);
629 $conf[\CURLOPT_VERBOSE] = true;
630 }
631 }
632
633 /**
634 * This function ensures that a response was set on a transaction. If one
635 * was not set, then the request is retried if possible. This error
636 * typically means you are sending a payload, curl encountered a
637 * "Connection died, retrying a fresh connect" error, tried to rewind the
638 * stream, and then encountered a "necessary data rewind wasn't possible"
639 * error, causing the request to be sent through curl_multi_info_read()
640 * without an error status.
641 *
642 * @param callable(RequestInterface, array): PromiseInterface $handler
643 */
644 private static function retryFailedRewind(callable $handler, EasyHandle $easy, array $ctx): PromiseInterface
645 {
646 try {
647 // Only rewind if the body has been read from.
648 $body = $easy->request->getBody();
649 if ($body->tell() > 0) {
650 $body->rewind();
651 }
652 } catch (\RuntimeException $e) {
653 $ctx['error'] = 'The connection unexpectedly failed without '
654 .'providing an error. The request would have been retried, '
655 .'but attempting to rewind the request body failed. '
656 .'Exception: '.$e;
657
658 return self::createRejection($easy, $ctx);
659 }
660
661 // Retry no more than 3 times before giving up.
662 if (!isset($easy->options['_curl_retries'])) {
663 $easy->options['_curl_retries'] = 1;
664 } elseif ($easy->options['_curl_retries'] == 2) {
665 $ctx['error'] = 'The cURL request was retried 3 times '
666 .'and did not succeed. The most likely reason for the failure '
667 .'is that cURL was unable to rewind the body of the request '
668 .'and subsequent retries resulted in the same error. Turn on '
669 .'the debug option to see what went wrong. See '
670 .'https://bugs.php.net/bug.php?id=47204 for more information.';
671
672 return self::createRejection($easy, $ctx);
673 } else {
674 ++$easy->options['_curl_retries'];
675 }
676
677 return $handler($easy->request, $easy->options);
678 }
679
680 private function createHeaderFn(EasyHandle $easy): callable
681 {
682 if (isset($easy->options['on_headers'])) {
683 $onHeaders = $easy->options['on_headers'];
684
685 if (!\is_callable($onHeaders)) {
686 throw new \InvalidArgumentException('on_headers must be callable');
687 }
688 } else {
689 $onHeaders = null;
690 }
691
692 return static function ($ch, $h) use (
693 $onHeaders,
694 $easy,
695 &$startingResponse
696 ) {
697 $value = \trim($h);
698 if ($value === '') {
699 $startingResponse = true;
700 try {
701 $easy->createResponse();
702 } catch (\Exception $e) {
703 $easy->createResponseException = $e;
704
705 return -1;
706 }
707 if ($onHeaders !== null) {
708 try {
709 $onHeaders($easy->response);
710 } catch (\Exception $e) {
711 // Associate the exception with the handle and trigger
712 // a curl header write error by returning 0.
713 $easy->onHeadersException = $e;
714
715 return -1;
716 }
717 }
718 } elseif ($startingResponse) {
719 $startingResponse = false;
720 $easy->headers = [$value];
721 } else {
722 $easy->headers[] = $value;
723 }
724
725 return \strlen($h);
726 };
727 }
728
729 public function __destruct()
730 {
731 foreach ($this->handles as $id => $handle) {
732 \curl_close($handle);
733 unset($this->handles[$id]);
734 }
735 }
736}
Note: See TracBrowser for help on using the repository browser.