MediaWiki master
MultiHttpClient.php
Go to the documentation of this file.
1<?php
9namespace Wikimedia\Http;
10
11use InvalidArgumentException;
13use Psr\Log\LoggerAwareInterface;
14use Psr\Log\LoggerInterface;
15use Psr\Log\NullLogger;
16use RuntimeException;
17
45class MultiHttpClient implements LoggerAwareInterface {
47 private const SENSITIVE_HEADERS = '/(^|-|_)(authorization|auth|password|cookie)($|-|_)/';
52 protected $cmh = null;
54 protected $caBundlePath;
56 protected $connTimeout = 10;
58 protected $maxConnTimeout = INF;
60 protected $reqTimeout = 30;
62 protected $maxReqTimeout = INF;
64 protected $usePipelining = false;
66 protected $maxConnsPerHost = 50;
68 protected $proxy;
70 protected $localProxy = false;
72 protected $localVirtualHosts = [];
74 protected $userAgent = 'wikimedia/multi-http-client v1.1';
76 protected $logger;
77 protected array $headers = [];
78
79 // In PHP 7 due to https://bugs.php.net/bug.php?id=76480 the request/connect
80 // timeouts are periodically polled instead of being accurately respected.
81 // The select timeout is set to the minimum timeout multiplied by this factor.
82 private const TIMEOUT_ACCURACY_FACTOR = 0.1;
83
84 private ?TelemetryHeadersInterface $telemetry = null;
85
108 public function __construct( array $options ) {
109 if ( isset( $options['caBundlePath'] ) ) {
110 $this->caBundlePath = $options['caBundlePath'];
111 if ( !file_exists( $this->caBundlePath ) ) {
112 throw new InvalidArgumentException( "Cannot find CA bundle: " . $this->caBundlePath );
113 }
114 }
115 static $opts = [
116 'connTimeout', 'maxConnTimeout', 'reqTimeout', 'maxReqTimeout',
117 'usePipelining', 'maxConnsPerHost', 'proxy', 'userAgent', 'logger',
118 'localProxy', 'localVirtualHosts', 'headers', 'telemetry'
119 ];
120 foreach ( $opts as $key ) {
121 if ( isset( $options[$key] ) ) {
122 $this->$key = $options[$key];
123 }
124 }
125 $this->logger ??= new NullLogger;
126 }
127
152 public function run( array $req, array $opts = [], string $caller = __METHOD__ ) {
153 return $this->runMulti( [ $req ], $opts, $caller )[0]['response'];
154 }
155
188 public function runMulti( array $reqs, array $opts = [], string $caller = __METHOD__ ) {
189 $this->normalizeRequests( $reqs );
190 $opts += [ 'connTimeout' => $this->connTimeout, 'reqTimeout' => $this->reqTimeout ];
191
192 if ( $this->maxConnTimeout && $opts['connTimeout'] > $this->maxConnTimeout ) {
193 $opts['connTimeout'] = $this->maxConnTimeout;
194 }
195 if ( $this->maxReqTimeout && $opts['reqTimeout'] > $this->maxReqTimeout ) {
196 $opts['reqTimeout'] = $this->maxReqTimeout;
197 }
198
199 if ( $this->isCurlEnabled() ) {
200 switch ( $opts['httpVersion'] ?? null ) {
201 case 'v1.0':
202 $opts['httpVersion'] = CURL_HTTP_VERSION_1_0;
203 break;
204 case 'v1.1':
205 $opts['httpVersion'] = CURL_HTTP_VERSION_1_1;
206 break;
207 case 'v2':
208 case 'v2.0':
209 $opts['httpVersion'] = CURL_HTTP_VERSION_2_0;
210 break;
211 case 'v3':
212 case 'v3.0':
213 $opts['httpVersion'] = CURL_HTTP_VERSION_3;
214 break;
215 default:
216 $opts['httpVersion'] = CURL_HTTP_VERSION_NONE;
217 }
218 return $this->runMultiCurl( $reqs, $opts, $caller );
219 } else {
220 # TODO: Add handling for httpVersion option
221 return $this->runMultiHttp( $reqs, $opts );
222 }
223 }
224
230 protected function isCurlEnabled() {
231 // Explicitly test if curl_multi* is blocked, as some users' hosts provide
232 // them with a modified curl with the multi-threaded parts removed(!)
233 return extension_loaded( 'curl' ) && function_exists( 'curl_multi_init' );
234 }
235
254 private function runMultiCurl( array $reqs, array $opts, string $caller = __METHOD__ ) {
255 $chm = $this->getCurlMulti( $opts );
256
257 $selectTimeout = $this->getSelectTimeout( $opts );
258
259 // Add all of the required cURL handles...
260 $handles = [];
261 foreach ( $reqs as $index => &$req ) {
262 $handles[$index] = $this->getCurlHandle( $req, $opts );
263 curl_multi_add_handle( $chm, $handles[$index] );
264 }
265 unset( $req ); // don't assign over this by accident
266
267 $infos = [];
268 // Execute the cURL handles concurrently...
269 $active = null; // handles still being processed
270 do {
271 // Do any available work...
272 $mrc = curl_multi_exec( $chm, $active );
273
274 if ( $mrc !== CURLM_OK ) {
275 $error = curl_multi_strerror( $mrc );
276 $this->logger->error( 'curl_multi_exec() failed: {error}', [
277 'error' => $error,
278 'exception' => new RuntimeException(),
279 'method' => $caller,
280 ] );
281 break;
282 }
283
284 // Wait (if possible) for available work...
285 if ( $active > 0 && curl_multi_select( $chm, $selectTimeout ) === -1 ) {
286 $errno = curl_multi_errno( $chm );
287 $error = curl_multi_strerror( $errno );
288 $this->logger->error( 'curl_multi_select() failed: {error}', [
289 'error' => $error,
290 'exception' => new RuntimeException(),
291 'method' => $caller,
292 ] );
293 }
294 } while ( $active > 0 );
295
296 $queuedMessages = null;
297 do {
298 $info = curl_multi_info_read( $chm, $queuedMessages );
299 if ( $info !== false && $info['msg'] === CURLMSG_DONE ) {
300 // Note: cast to integer even works on PHP 8.0+ despite the
301 // handle being an object not a resource, because CurlHandle
302 // has a backwards-compatible cast_object handler.
303 $infos[(int)$info['handle']] = $info;
304 }
305 } while ( $queuedMessages > 0 );
306
307 // Remove all of the added cURL handles and check for errors...
308 foreach ( $reqs as $index => &$req ) {
309 $ch = $handles[$index];
310 curl_multi_remove_handle( $chm, $ch );
311
312 if ( isset( $infos[(int)$ch] ) ) {
313 $info = $infos[(int)$ch];
314 $errno = $info['result'];
315 if ( $errno !== 0 ) {
316 $req['response']['error'] = "(curl error: $errno)";
317 if ( function_exists( 'curl_strerror' ) ) {
318 $req['response']['error'] .= " " . curl_strerror( $errno );
319 }
320 $this->logger->error( 'Error fetching URL "{url}": {error}', [
321 'url' => $req['url'],
322 'error' => $req['response']['error'],
323 'exception' => new RuntimeException(),
324 'method' => $caller,
325 ] );
326 } else {
327 $this->logger->debug(
328 "HTTP complete: {method} {url} code={response_code} size={size} " .
329 "total={total_time} connect={connect_time}",
330 [
331 'method' => $req['method'],
332 'url' => $req['url'],
333 'response_code' => $req['response']['code'],
334 'size' => curl_getinfo( $ch, CURLINFO_SIZE_DOWNLOAD ),
335 'total_time' => $this->getCurlTime(
336 $ch, CURLINFO_TOTAL_TIME, 'CURLINFO_TOTAL_TIME_T'
337 ),
338 'connect_time' => $this->getCurlTime(
339 $ch, CURLINFO_CONNECT_TIME, 'CURLINFO_CONNECT_TIME_T'
340 ),
341 ]
342 );
343 }
344 } else {
345 $req['response']['error'] = "(curl error: no status set)";
346 }
347
348 // For convenience with array destructuring
349 $req['response'][0] = $req['response']['code'];
350 $req['response'][1] = $req['response']['reason'];
351 $req['response'][2] = $req['response']['headers'];
352 $req['response'][3] = $req['response']['body'];
353 $req['response'][4] = $req['response']['error'];
354 // Close any string wrapper file handles
355 if ( isset( $req['_closeHandle'] ) ) {
356 fclose( $req['_closeHandle'] );
357 unset( $req['_closeHandle'] );
358 }
359 }
360 unset( $req ); // don't assign over this by accident
361
362 return $reqs;
363 }
364
377 protected function getCurlHandle( array &$req, array $opts ) {
378 $ch = curl_init();
379
380 curl_setopt( $ch, CURLOPT_PROXY, $req['proxy'] ?? $this->proxy );
381 curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT_MS, intval( $opts['connTimeout'] * 1e3 ) );
382 curl_setopt( $ch, CURLOPT_TIMEOUT_MS, intval( $opts['reqTimeout'] * 1e3 ) );
383 curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 );
384 curl_setopt( $ch, CURLOPT_MAXREDIRS, 4 );
385 curl_setopt( $ch, CURLOPT_HEADER, 0 );
386 if ( $this->caBundlePath !== null ) {
387 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
388 curl_setopt( $ch, CURLOPT_CAINFO, $this->caBundlePath );
389 }
390 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
391
392 $url = $req['url'];
393 $query = http_build_query( $req['query'], '', '&', PHP_QUERY_RFC3986 );
394 if ( $query != '' ) {
395 $url .= !str_contains( $req['url'], '?' ) ? "?$query" : "&$query";
396 }
397 curl_setopt( $ch, CURLOPT_URL, $url );
398 curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, $req['method'] );
399 curl_setopt( $ch, CURLOPT_NOBODY, ( $req['method'] === 'HEAD' ) );
400 curl_setopt( $ch, CURLOPT_HTTP_VERSION, $opts['httpVersion'] ?? CURL_HTTP_VERSION_NONE );
401
402 if ( $req['method'] === 'PUT' ) {
403 curl_setopt( $ch, CURLOPT_PUT, 1 );
404 // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.is_resource
405 if ( is_resource( $req['body'] ) ) {
406 curl_setopt( $ch, CURLOPT_INFILE, $req['body'] );
407 if ( isset( $req['headers']['content-length'] ) ) {
408 curl_setopt( $ch, CURLOPT_INFILESIZE, $req['headers']['content-length'] );
409 } elseif ( isset( $req['headers']['transfer-encoding'] ) &&
410 $req['headers']['transfer-encoding'] === 'chunks'
411 ) {
412 curl_setopt( $ch, CURLOPT_UPLOAD, true );
413 } else {
414 throw new InvalidArgumentException( "Missing 'Content-Length' or 'Transfer-Encoding' header." );
415 }
416 } elseif ( $req['body'] !== '' ) {
417 $fp = fopen( "php://temp", "wb+" );
418 fwrite( $fp, $req['body'], strlen( $req['body'] ) );
419 rewind( $fp );
420 curl_setopt( $ch, CURLOPT_INFILE, $fp );
421 curl_setopt( $ch, CURLOPT_INFILESIZE, strlen( $req['body'] ) );
422 $req['_closeHandle'] = $fp; // remember to close this later
423 } else {
424 curl_setopt( $ch, CURLOPT_INFILESIZE, 0 );
425 }
426 curl_setopt( $ch, CURLOPT_READFUNCTION,
427 static function ( $ch, $fd, $length ) {
428 return (string)fread( $fd, $length );
429 }
430 );
431 } elseif ( $req['method'] === 'POST' ) {
432 curl_setopt( $ch, CURLOPT_POST, 1 );
433 curl_setopt( $ch, CURLOPT_POSTFIELDS, $req['body'] );
434 } else {
435 // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.is_resource
436 if ( is_resource( $req['body'] ) || $req['body'] !== '' ) {
437 throw new InvalidArgumentException( "HTTP body specified for a non PUT/POST request." );
438 }
439 $req['headers']['content-length'] = 0;
440 }
441
442 if ( !isset( $req['headers']['user-agent'] ) ) {
443 $req['headers']['user-agent'] = $this->userAgent;
444 }
445
446 $headers = [];
447 foreach ( $req['headers'] as $name => $value ) {
448 if ( str_contains( $name, ':' ) ) {
449 throw new InvalidArgumentException( "Header name must not contain colon-space." );
450 }
451 $headers[] = $name . ': ' . trim( $value );
452 }
453 curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
454
455 curl_setopt( $ch, CURLOPT_HEADERFUNCTION,
456 static function ( $ch, $header ) use ( &$req ) {
457 if ( !empty( $req['flags']['relayResponseHeaders'] ) && trim( $header ) !== '' ) {
458 header( $header );
459 }
460 $length = strlen( $header );
461 $matches = [];
462 if ( preg_match( "/^(HTTP\/(?:1\.[01]|2|3)) (\d{3}) (.*)/", $header, $matches ) ) {
463 $req['response']['code'] = (int)$matches[2];
464 $req['response']['reason'] = trim( $matches[3] );
465 // After a redirect we will receive this again, but we already stored headers
466 // that belonged to a redirect response. Start over.
467 $req['response']['headers'] = [];
468 return $length;
469 }
470 if ( !str_contains( $header, ":" ) ) {
471 return $length;
472 }
473 [ $name, $value ] = explode( ":", $header, 2 );
474 $name = strtolower( $name );
475 $value = trim( $value );
476 if ( isset( $req['response']['headers'][$name] ) ) {
477 $req['response']['headers'][$name] .= ', ' . $value;
478 } else {
479 $req['response']['headers'][$name] = $value;
480 }
481 return $length;
482 }
483 );
484
485 // This works with both file and php://temp handles (unlike CURLOPT_FILE)
486 $hasOutputStream = isset( $req['stream'] );
487 curl_setopt( $ch, CURLOPT_WRITEFUNCTION,
488 static function ( $ch, $data ) use ( &$req, $hasOutputStream ) {
489 if ( $hasOutputStream ) {
490 return fwrite( $req['stream'], $data );
491 } else {
492 $req['response']['body'] .= $data;
493
494 return strlen( $data );
495 }
496 }
497 );
498
499 return $ch;
500 }
501
508 protected function getCurlMulti( array $opts ) {
509 if ( !$this->cmh ) {
510 $cmh = curl_multi_init();
511 // Limit the size of the idle connection cache such that consecutive parallel
512 // request batches to the same host can avoid having to keep making connections
513 curl_multi_setopt( $cmh, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
514 $this->cmh = $cmh;
515 }
516
517 $curlVersion = curl_version()['version'];
518
519 // CURLMOPT_MAX_HOST_CONNECTIONS is available since PHP 7.0.7 and cURL 7.30.0
520 if ( version_compare( $curlVersion, '7.30.0', '>=' ) ) {
521 // Limit the number of in-flight requests for any given host
522 $maxHostConns = $opts['maxConnsPerHost'] ?? $this->maxConnsPerHost;
523 curl_multi_setopt( $this->cmh, CURLMOPT_MAX_HOST_CONNECTIONS, (int)$maxHostConns );
524 }
525
526 if ( $opts['usePipelining'] ?? $this->usePipelining ) {
527 if ( version_compare( $curlVersion, '7.43', '<' ) ) {
528 // The option is a boolean
529 $pipelining = 1;
530 } elseif ( version_compare( $curlVersion, '7.62', '<' ) ) {
531 // The option is a bitfield and HTTP/1.x pipelining is supported
532 $pipelining = CURLPIPE_HTTP1 | CURLPIPE_MULTIPLEX;
533 } else {
534 // The option is a bitfield but HTTP/1.x pipelining has been removed
535 $pipelining = CURLPIPE_MULTIPLEX;
536 }
537 // Suppress deprecation, we know already (T264735)
538 // phpcs:ignore Generic.PHP.NoSilencedErrors
539 @curl_multi_setopt( $this->cmh, CURLMOPT_PIPELINING, $pipelining );
540 }
541
542 return $this->cmh;
543 }
544
555 private function getCurlTime( $ch, $oldOption, $newConstName ): string {
556 if ( defined( $newConstName ) ) {
557 return sprintf( "%.6F", curl_getinfo( $ch, constant( $newConstName ) ) / 1e6 );
558 } else {
559 return (string)curl_getinfo( $ch, $oldOption );
560 }
561 }
562
578 private function runMultiHttp( array $reqs, array $opts = [] ) {
579 $httpOptions = [
580 'timeout' => $opts['reqTimeout'] ?? $this->reqTimeout,
581 'connectTimeout' => $opts['connTimeout'] ?? $this->connTimeout,
582 'logger' => $this->logger,
583 'caInfo' => $this->caBundlePath,
584 ];
585 foreach ( $reqs as &$req ) {
586 $reqOptions = $httpOptions + [
587 'method' => $req['method'],
588 'proxy' => $req['proxy'] ?? $this->proxy,
589 'userAgent' => $req['headers']['user-agent'] ?? $this->userAgent,
590 'postData' => $req['body'],
591 ];
592
593 $url = $req['url'];
594 $query = http_build_query( $req['query'], '', '&', PHP_QUERY_RFC3986 );
595 if ( $query != '' ) {
596 $url .= !str_contains( $req['url'], '?' ) ? "?$query" : "&$query";
597 }
598
599 $httpRequest = MediaWikiServices::getInstance()->getHttpRequestFactory()->create(
600 $url, $reqOptions, __METHOD__ );
601 $httpRequest->setLogger( $this->logger );
602 foreach ( $req['headers'] as $header => $value ) {
603 $httpRequest->setHeader( $header, $value );
604 }
605 $sv = $httpRequest->execute()->getStatusValue();
606
607 $respHeaders = array_map(
608 static fn ( $v ) => implode( ', ', $v ),
609 $httpRequest->getResponseHeaders() );
610
611 $req['response'] = [
612 'code' => $httpRequest->getStatus(),
613 'reason' => '',
614 'headers' => $respHeaders,
615 'body' => $httpRequest->getContent(),
616 'error' => '',
617 ];
618
619 if ( !$sv->isOK() ) {
620 $svErrors = $sv->getErrors();
621 if ( isset( $svErrors[0] ) ) {
622 $req['response']['error'] = $svErrors[0]['message'];
623
624 // param values vary per failure type (ex. unknown host vs unknown page)
625 if ( isset( $svErrors[0]['params'][0] ) ) {
626 if ( is_numeric( $svErrors[0]['params'][0] ) ) {
627 if ( isset( $svErrors[0]['params'][1] ) ) {
628 // @phan-suppress-next-line PhanTypeInvalidDimOffset
629 $req['response']['reason'] = $svErrors[0]['params'][1];
630 }
631 } else {
632 $req['response']['reason'] = $svErrors[0]['params'][0];
633 }
634 }
635 }
636 }
637
638 $req['response'][0] = $req['response']['code'];
639 $req['response'][1] = $req['response']['reason'];
640 $req['response'][2] = $req['response']['headers'];
641 $req['response'][3] = $req['response']['body'];
642 $req['response'][4] = $req['response']['error'];
643 }
644
645 return $reqs;
646 }
647
653 private function normalizeHeaders( array $headers ): array {
654 $normalized = [];
655 foreach ( $headers as $name => $value ) {
656 $normalized[strtolower( $name )] = $value;
657 }
658 return $normalized;
659 }
660
666 private function normalizeRequests( array &$reqs ) {
667 foreach ( $reqs as &$req ) {
668 $req['response'] = [
669 'code' => 0,
670 'reason' => '',
671 'headers' => [],
672 'body' => '',
673 'error' => ''
674 ];
675 if ( isset( $req[0] ) ) {
676 $req['method'] = $req[0]; // short-form
677 unset( $req[0] );
678 }
679 if ( isset( $req[1] ) ) {
680 $req['url'] = $req[1]; // short-form
681 unset( $req[1] );
682 }
683 if ( !isset( $req['method'] ) ) {
684 throw new InvalidArgumentException( "Request has no 'method' field set." );
685 } elseif ( !isset( $req['url'] ) ) {
686 throw new InvalidArgumentException( "Request has no 'url' field set." );
687 }
688 if ( $this->localProxy !== false && $this->isLocalURL( $req['url'] ) ) {
689 $this->useReverseProxy( $req, $this->localProxy );
690 }
691 $req['query'] ??= [];
692 $req['headers'] = $this->normalizeHeaders(
693 array_merge(
694 $this->headers,
695 $this->telemetry ? $this->telemetry->getRequestHeaders() : [],
696 $req['headers'] ?? []
697 )
698 );
699
700 if ( !isset( $req['body'] ) ) {
701 $req['body'] = '';
702 $req['headers']['content-length'] = 0;
703 }
704 // Redact some headers we know to have tokens before logging them
705 $logHeaders = $req['headers'];
706 foreach ( $logHeaders as $header => $value ) {
707 if ( preg_match( self::SENSITIVE_HEADERS, $header ) === 1 ) {
708 $logHeaders[$header] = '[redacted]';
709 }
710 }
711 $this->logger->debug( "HTTP start: {method} {url}",
712 [
713 'method' => $req['method'],
714 'url' => $req['url'],
715 'headers' => $logHeaders,
716 ]
717 );
718 $req['flags'] ??= [];
719 }
720 }
721
722 private function useReverseProxy( array &$req, string $proxy ) {
723 $parsedProxy = parse_url( $proxy );
724 if ( $parsedProxy === false ) {
725 throw new InvalidArgumentException( "Invalid reverseProxy configured: $proxy" );
726 }
727 $parsedUrl = parse_url( $req['url'] );
728 if ( $parsedUrl === false ) {
729 throw new InvalidArgumentException( "Invalid url specified: {$req['url']}" );
730 }
731 // Set the current host in the Host header
732 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
733 $req['headers']['Host'] = $parsedUrl['host'];
734 // Replace scheme, host and port in the request
735 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
736 $parsedUrl['scheme'] = $parsedProxy['scheme'];
737 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
738 $parsedUrl['host'] = $parsedProxy['host'];
739 if ( isset( $parsedProxy['port'] ) ) {
740 $parsedUrl['port'] = $parsedProxy['port'];
741 } else {
742 unset( $parsedUrl['port'] );
743 }
744 $req['url'] = self::assembleUrl( $parsedUrl );
745 // Explicitly disable use of another proxy by setting to false,
746 // since null will fallback to $this->proxy
747 $req['proxy'] = false;
748 }
749
760 private static function assembleUrl( array $urlParts ): string {
761 $result = isset( $urlParts['scheme'] ) ? $urlParts['scheme'] . '://' : '';
762
763 if ( isset( $urlParts['host'] ) ) {
764 if ( isset( $urlParts['user'] ) ) {
765 $result .= $urlParts['user'];
766 if ( isset( $urlParts['pass'] ) ) {
767 $result .= ':' . $urlParts['pass'];
768 }
769 $result .= '@';
770 }
771
772 $result .= $urlParts['host'];
773
774 if ( isset( $urlParts['port'] ) ) {
775 $result .= ':' . $urlParts['port'];
776 }
777 }
778
779 if ( isset( $urlParts['path'] ) ) {
780 $result .= $urlParts['path'];
781 }
782
783 if ( isset( $urlParts['query'] ) && $urlParts['query'] !== '' ) {
784 $result .= '?' . $urlParts['query'];
785 }
786
787 if ( isset( $urlParts['fragment'] ) ) {
788 $result .= '#' . $urlParts['fragment'];
789 }
790
791 return $result;
792 }
793
801 private function isLocalURL( $url ) {
802 if ( !$this->localVirtualHosts ) {
803 // Shortcut
804 return false;
805 }
806
807 // Extract host part
808 $matches = [];
809 if ( preg_match( '!^https?://([\w.-]+)[/:].*$!', $url, $matches ) ) {
810 $host = $matches[1];
811 // Split up dotwise
812 $domainParts = explode( '.', $host );
813 // Check if this domain or any superdomain is listed as a local virtual host
814 $domainParts = array_reverse( $domainParts );
815
816 $domain = '';
817 $countParts = count( $domainParts );
818 for ( $i = 0; $i < $countParts; $i++ ) {
819 $domainPart = $domainParts[$i];
820 if ( $i == 0 ) {
821 $domain = $domainPart;
822 } else {
823 $domain = $domainPart . '.' . $domain;
824 }
825
826 if ( in_array( $domain, $this->localVirtualHosts ) ) {
827 return true;
828 }
829 }
830 }
831
832 return false;
833 }
834
841 private function getSelectTimeout( $opts ) {
842 $connTimeout = $opts['connTimeout'] ?? $this->connTimeout;
843 $reqTimeout = $opts['reqTimeout'] ?? $this->reqTimeout;
844 $timeouts = array_filter( [ $connTimeout, $reqTimeout ] );
845 if ( count( $timeouts ) === 0 ) {
846 return 1;
847 }
848
849 $selectTimeout = min( $timeouts ) * self::TIMEOUT_ACCURACY_FACTOR;
850 // Minimum 10us
851 if ( $selectTimeout < 10e-6 ) {
852 $selectTimeout = 10e-6;
853 }
854 return $selectTimeout;
855 }
856
860 public function setLogger( LoggerInterface $logger ): void {
861 $this->logger = $logger;
862 }
863
864 public function __destruct() {
865 if ( $this->cmh ) {
866 curl_multi_close( $this->cmh );
867 $this->cmh = null;
868 }
869 }
870
871}
873class_alias( MultiHttpClient::class, 'MultiHttpClient' );
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
Service locator for MediaWiki core services.
Class to handle multiple HTTP requests.
resource object null $cmh
curl_multi_init() handle, initialized in getCurlMulti()
__construct(array $options)
Since 1.35, callers should use HttpRequestFactory::createMultiClient() to get a client object with ap...
runMulti(array $reqs, array $opts=[], string $caller=__METHOD__)
Execute a set of HTTP(S) requests.
string null $caBundlePath
SSL certificates path.
getCurlHandle(array &$req, array $opts)
setLogger(LoggerInterface $logger)
Register a logger.
isCurlEnabled()
Determines if the curl extension is available.
run(array $req, array $opts=[], string $caller=__METHOD__)
Execute an HTTP(S) request.
Provide Request Telemetry information.
Utility for parsing a HTTP Accept header value into a weight map.