* Add HttpRequestFactory::createMultiClient(), which returns a MultiHttpClient with configured defaults applied. This is similar to the recently-deprecated Http::createMultiClient(). * Introduce $wgHTTPMaxTimeout and $wgHTTPMaxConnectTimeout which, if set to a lower value than their defaults of infinity, will limit the applied HTTP timeouts, whether configured or passed on a per-request basis. This is based on the frequently correct assumption that ops know more about timeouts than developers. * In case developers believe, after becoming aware of this new situation, that they actually do know more about timeouts than ops, it is possible to override the configured maximum by passing similarly named options to HttpRequestFactory::createMultiClient() and HttpRequestFactory::create(). * Apply modern standards to HttpRequestFactory by injecting a logger and all configuration parameters used by its backends. * As in Http, the new createMultiClient() will use a MediaWiki/1.35 User-Agent and the 'http' channel for logging. * Document that no proxy will be used for createMultiClient(). Proxy config is weird and was previously a good reason to use MultiHttpClient over HttpRequestFactory. * Deprecate direct construction of MWHttpRequest without a timeout parameter Bug: T245170 Change-Id: I8252f6c854b98059f4916d5460ea71cf4b580149
281 lines
7.4 KiB
PHP
281 lines
7.4 KiB
PHP
<?php
|
|
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
|
|
/**
|
|
* The urls herein are not actually called, because we mock the return results.
|
|
*
|
|
* @covers MultiHttpClient
|
|
*/
|
|
class MultiHttpClientTest extends MediaWikiTestCase {
|
|
/** @var MultiHttpClient|MockObject */
|
|
protected $client;
|
|
|
|
/** @return MultiHttpClient|MockObject */
|
|
private function createClient( $options = [] ) {
|
|
$client = $this->getMockBuilder( MultiHttpClient::class )
|
|
->setConstructorArgs( [ $options ] )
|
|
->setMethods( [ 'isCurlEnabled' ] )->getMock();
|
|
$client->method( 'isCurlEnabled' )->willReturn( false );
|
|
return $client;
|
|
}
|
|
|
|
protected function setUp() : void {
|
|
parent::setUp();
|
|
$this->client = $this->createClient( [] );
|
|
}
|
|
|
|
private function getHttpRequest( $statusValue, $statusCode, $headers = [] ) {
|
|
$options = [
|
|
'timeout' => 1,
|
|
'connectTimeout' => 1
|
|
];
|
|
$httpRequest = $this->getMockBuilder( PhpHttpRequest::class )
|
|
->setConstructorArgs( [ '', $options ] )
|
|
->getMock();
|
|
$httpRequest->expects( $this->any() )
|
|
->method( 'execute' )
|
|
->willReturn( Status::wrap( $statusValue ) );
|
|
$httpRequest->expects( $this->any() )
|
|
->method( 'getResponseHeaders' )
|
|
->willReturn( $headers );
|
|
$httpRequest->expects( $this->any() )
|
|
->method( 'getStatus' )
|
|
->willReturn( $statusCode );
|
|
return $httpRequest;
|
|
}
|
|
|
|
private function mockHttpRequestFactory( $httpRequest ) {
|
|
$factory = $this->getMockBuilder( MediaWiki\Http\HttpRequestFactory::class )
|
|
->disableOriginalConstructor()
|
|
->getMock();
|
|
$factory->expects( $this->any() )
|
|
->method( 'create' )
|
|
->willReturn( $httpRequest );
|
|
return $factory;
|
|
}
|
|
|
|
/**
|
|
* Test call of a single url that should succeed
|
|
*/
|
|
public function testMultiHttpClientSingleSuccess() {
|
|
// Mock success
|
|
$httpRequest = $this->getHttpRequest( StatusValue::newGood( 200 ), 200 );
|
|
$this->setService( 'HttpRequestFactory', $this->mockHttpRequestFactory( $httpRequest ) );
|
|
|
|
list( $rcode, $rdesc, /* $rhdrs */, $rbody, $rerr ) = $this->client->run( [
|
|
'method' => 'GET',
|
|
'url' => "http://example.test",
|
|
] );
|
|
|
|
$this->assertEquals( 200, $rcode );
|
|
}
|
|
|
|
/**
|
|
* Test call of a single url that should not exist, and therefore fail
|
|
*/
|
|
public function testMultiHttpClientSingleFailure() {
|
|
// Mock an invalid tld
|
|
$httpRequest = $this->getHttpRequest(
|
|
StatusValue::newFatal( 'http-invalid-url', 'http://www.example.test' ), 0 );
|
|
$this->setService( 'HttpRequestFactory', $this->mockHttpRequestFactory( $httpRequest ) );
|
|
|
|
list( $rcode, $rdesc, /* $rhdrs */, $rbody, $rerr ) = $this->client->run( [
|
|
'method' => 'GET',
|
|
'url' => "http://www.example.test",
|
|
] );
|
|
|
|
$failure = $rcode < 200 || $rcode >= 400;
|
|
$this->assertTrue( $failure );
|
|
}
|
|
|
|
/**
|
|
* Test call of multiple urls that should all succeed
|
|
*/
|
|
public function testMultiHttpClientMultipleSuccess() {
|
|
// Mock success
|
|
$httpRequest = $this->getHttpRequest( StatusValue::newGood( 200 ), 200 );
|
|
$this->setService( 'HttpRequestFactory', $this->mockHttpRequestFactory( $httpRequest ) );
|
|
|
|
$reqs = [
|
|
[
|
|
'method' => 'GET',
|
|
'url' => 'http://example.test',
|
|
],
|
|
[
|
|
'method' => 'GET',
|
|
'url' => 'https://get.test',
|
|
],
|
|
];
|
|
$responses = $this->client->runMulti( $reqs );
|
|
foreach ( $responses as $response ) {
|
|
list( $rcode, $rdesc, /* $rhdrs */, $rbody, $rerr ) = $response['response'];
|
|
$this->assertEquals( 200, $rcode );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test call of multiple urls that should all fail
|
|
*/
|
|
public function testMultiHttpClientMultipleFailure() {
|
|
// Mock page not found
|
|
$httpRequest = $this->getHttpRequest(
|
|
StatusValue::newFatal( "http-bad-status", 404, 'Not Found' ), 404 );
|
|
$this->setService( 'HttpRequestFactory', $this->mockHttpRequestFactory( $httpRequest ) );
|
|
|
|
$reqs = [
|
|
[
|
|
'method' => 'GET',
|
|
'url' => 'http://example.test/12345',
|
|
],
|
|
[
|
|
'method' => 'GET',
|
|
'url' => 'http://example.test/67890' ,
|
|
]
|
|
];
|
|
$responses = $this->client->runMulti( $reqs );
|
|
foreach ( $responses as $response ) {
|
|
list( $rcode, $rdesc, /* $rhdrs */, $rbody, $rerr ) = $response['response'];
|
|
$failure = $rcode < 200 || $rcode >= 400;
|
|
$this->assertTrue( $failure );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test of response header handling
|
|
*/
|
|
public function testMultiHttpClientHeaders() {
|
|
// Represenative headers for typical requests, per MWHttpRequest::getResponseHeaders()
|
|
$headers = [
|
|
'content-type' => [
|
|
'text/html; charset=utf-8',
|
|
],
|
|
'date' => [
|
|
'Wed, 18 Jul 2018 14:52:41 GMT',
|
|
],
|
|
'set-cookie' => [
|
|
'COUNTRY=NAe6; expires=Wed, 25-Jul-2018 14:52:41 GMT; path=/; domain=.example.test',
|
|
'LAST_NEWS=1531925562; expires=Thu, 18-Jul-2019 14:52:41 GMT; path=/; domain=.example.test',
|
|
]
|
|
];
|
|
|
|
// Mock success with specific headers
|
|
$httpRequest = $this->getHttpRequest( StatusValue::newGood( 200 ), 200, $headers );
|
|
$this->setService( 'HttpRequestFactory', $this->mockHttpRequestFactory( $httpRequest ) );
|
|
|
|
list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->client->run( [
|
|
'method' => 'GET',
|
|
'url' => 'http://example.test',
|
|
] );
|
|
|
|
$this->assertEquals( 200, $rcode );
|
|
$this->assertEquals( count( $headers ), count( $rhdrs ) );
|
|
foreach ( $headers as $name => $values ) {
|
|
$value = implode( ', ', $values );
|
|
$this->assertArrayHasKey( $name, $rhdrs );
|
|
$this->assertEquals( $value, $rhdrs[$name] );
|
|
}
|
|
}
|
|
|
|
public static function provideMultiHttpTimeout() {
|
|
return [
|
|
'default 10/900' => [
|
|
[],
|
|
[],
|
|
// This can be changed per a re-evaluation of T226979, but if
|
|
// it's less than 2/3 then the default tests below would have
|
|
// to be updated
|
|
10,
|
|
900
|
|
],
|
|
'constructor override' => [
|
|
[ 'connTimeout' => 2, 'reqTimeout' => 3 ],
|
|
[],
|
|
2,
|
|
3
|
|
],
|
|
'run override' => [
|
|
[],
|
|
[ 'connTimeout' => 2, 'reqTimeout' => 3 ],
|
|
2,
|
|
3
|
|
],
|
|
'constructor max option limits default' => [
|
|
[ 'maxConnTimeout' => 2, 'maxReqTimeout' => 3 ],
|
|
[],
|
|
2,
|
|
3
|
|
],
|
|
'constructor max option limits regular constructor option' => [
|
|
[
|
|
'maxConnTimeout' => 2,
|
|
'maxReqTimeout' => 3,
|
|
'connTimeout' => 100,
|
|
'reqTimeout' => 100
|
|
],
|
|
[],
|
|
2,
|
|
3
|
|
],
|
|
'constructor max option greater than regular constructor option' => [
|
|
[
|
|
'maxConnTimeout' => 2,
|
|
'maxReqTimeout' => 3,
|
|
'connTimeout' => 1,
|
|
'reqTimeout' => 1
|
|
],
|
|
[],
|
|
1,
|
|
1
|
|
],
|
|
'constructor max option limits run option' => [
|
|
[
|
|
'maxConnTimeout' => 2,
|
|
'maxReqTimeout' => 3,
|
|
],
|
|
[
|
|
'connTimeout' => 100,
|
|
'reqTimeout' => 100
|
|
],
|
|
2,
|
|
3
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Test of timeout parameter handling
|
|
* @dataProvider provideMultiHttpTimeout
|
|
*/
|
|
public function testMultiHttpTimeout( $createOptions, $runOptions,
|
|
$expectedConnTimeout, $expectedReqTimeout
|
|
) {
|
|
$url = 'http://www.example.test';
|
|
$httpRequest = $this->getHttpRequest( StatusValue::newGood( 200 ), 200 );
|
|
$factory = $this->getMockBuilder( MediaWiki\Http\HttpRequestFactory::class )
|
|
->disableOriginalConstructor()
|
|
->getMock();
|
|
$factory->expects( $this->any() )
|
|
->method( 'create' )
|
|
->with(
|
|
$url,
|
|
$this->callback(
|
|
function ( $options ) use ( $expectedReqTimeout, $expectedConnTimeout ) {
|
|
return $options['timeout'] === $expectedReqTimeout
|
|
&& $options['connectTimeout'] === $expectedConnTimeout;
|
|
}
|
|
)
|
|
)
|
|
->willReturn( $httpRequest );
|
|
$this->setService( 'HttpRequestFactory', $factory );
|
|
|
|
$client = $this->createClient( $createOptions );
|
|
|
|
$client->run(
|
|
[ 'method' => 'GET', 'url' => $url ],
|
|
$runOptions
|
|
);
|
|
|
|
$this->assertTrue( true );
|
|
}
|
|
}
|