* 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
228 lines
7 KiB
PHP
228 lines
7 KiB
PHP
<?php
|
|
|
|
use GuzzleHttp\Handler\MockHandler;
|
|
use GuzzleHttp\HandlerStack;
|
|
use GuzzleHttp\Middleware;
|
|
use GuzzleHttp\Psr7\Request;
|
|
use GuzzleHttp\Psr7\Response;
|
|
|
|
/**
|
|
* class for tests of GuzzleHttpRequest
|
|
*
|
|
* No actual requests are made herein - all external communications are mocked
|
|
*
|
|
* @covers GuzzleHttpRequest
|
|
* @covers MWHttpRequest
|
|
*/
|
|
class GuzzleHttpRequestTest extends MediaWikiTestCase {
|
|
/** @var int[] */
|
|
private $timeoutOptions = [
|
|
'timeout' => 1,
|
|
'connectTimeout' => 1
|
|
];
|
|
|
|
/**
|
|
* Placeholder url to use for various tests. This is never contacted, but we must use
|
|
* a url of valid format to avoid validation errors.
|
|
* @var string
|
|
*/
|
|
protected $exampleUrl = 'http://www.example.test';
|
|
|
|
/**
|
|
* Minimal example body text
|
|
* @var string
|
|
*/
|
|
protected $exampleBodyText = 'x';
|
|
|
|
/**
|
|
* For accumulating callback data for testing
|
|
* @var string
|
|
*/
|
|
protected $bodyTextReceived = '';
|
|
|
|
/**
|
|
* Callback: process a chunk of the result of a HTTP request
|
|
*
|
|
* @param mixed $req
|
|
* @param string $buffer
|
|
* @return int Number of bytes handled
|
|
*/
|
|
public function processHttpDataChunk( $req, $buffer ) {
|
|
$this->bodyTextReceived .= $buffer;
|
|
return strlen( $buffer );
|
|
}
|
|
|
|
public function testSuccess() {
|
|
$handler = HandlerStack::create( new MockHandler( [ new Response( 200, [
|
|
'status' => 200,
|
|
], $this->exampleBodyText ) ] ) );
|
|
$r = new GuzzleHttpRequest( $this->exampleUrl,
|
|
[ 'handler' => $handler ] + $this->timeoutOptions );
|
|
$r->execute();
|
|
|
|
$this->assertEquals( 200, $r->getStatus() );
|
|
$this->assertEquals( $this->exampleBodyText, $r->getContent() );
|
|
}
|
|
|
|
public function testSuccessConstructorCallback() {
|
|
$this->bodyTextReceived = '';
|
|
$handler = HandlerStack::create( new MockHandler( [ new Response( 200, [
|
|
'status' => 200,
|
|
], $this->exampleBodyText ) ] ) );
|
|
$r = new GuzzleHttpRequest( $this->exampleUrl, [
|
|
'callback' => [ $this, 'processHttpDataChunk' ],
|
|
'handler' => $handler,
|
|
] + $this->timeoutOptions );
|
|
$r->execute();
|
|
|
|
$this->assertEquals( 200, $r->getStatus() );
|
|
$this->assertEquals( $this->exampleBodyText, $this->bodyTextReceived );
|
|
}
|
|
|
|
public function testSuccessSetCallback() {
|
|
$this->bodyTextReceived = '';
|
|
$handler = HandlerStack::create( new MockHandler( [ new Response( 200, [
|
|
'status' => 200,
|
|
], $this->exampleBodyText ) ] ) );
|
|
$r = new GuzzleHttpRequest( $this->exampleUrl, [
|
|
'handler' => $handler,
|
|
] + $this->timeoutOptions );
|
|
$r->setCallback( [ $this, 'processHttpDataChunk' ] );
|
|
$r->execute();
|
|
|
|
$this->assertEquals( 200, $r->getStatus() );
|
|
$this->assertEquals( $this->exampleBodyText, $this->bodyTextReceived );
|
|
}
|
|
|
|
/**
|
|
* use a callback stream to pipe the mocked response data to our callback function
|
|
*/
|
|
public function testSuccessSink() {
|
|
$this->bodyTextReceived = '';
|
|
$handler = HandlerStack::create( new MockHandler( [ new Response( 200, [
|
|
'status' => 200,
|
|
], $this->exampleBodyText ) ] ) );
|
|
$r = new GuzzleHttpRequest( $this->exampleUrl, [
|
|
'handler' => $handler,
|
|
'sink' => new MWCallbackStream( [ $this, 'processHttpDataChunk' ] ),
|
|
] + $this->timeoutOptions );
|
|
$r->execute();
|
|
|
|
$this->assertEquals( 200, $r->getStatus() );
|
|
$this->assertEquals( $this->exampleBodyText, $this->bodyTextReceived );
|
|
}
|
|
|
|
public function testBadUrl() {
|
|
$r = new GuzzleHttpRequest( '', $this->timeoutOptions );
|
|
$s = $r->execute();
|
|
$errorMsg = $s->getErrorsByType( 'error' )[0]['message'];
|
|
|
|
$this->assertSame( 0, $r->getStatus() );
|
|
$this->assertEquals( 'http-invalid-url', $errorMsg );
|
|
}
|
|
|
|
public function testConnectException() {
|
|
$handler = HandlerStack::create( new MockHandler( [ new GuzzleHttp\Exception\ConnectException(
|
|
'Mock Connection Exception', new Request( 'GET', $this->exampleUrl )
|
|
) ] ) );
|
|
$r = new GuzzleHttpRequest( $this->exampleUrl,
|
|
[ 'handler' => $handler ] + $this->timeoutOptions );
|
|
$s = $r->execute();
|
|
$errorMsg = $s->getErrorsByType( 'error' )[0]['message'];
|
|
|
|
$this->assertSame( 0, $r->getStatus() );
|
|
$this->assertEquals( 'http-request-error', $errorMsg );
|
|
}
|
|
|
|
public function testTimeout() {
|
|
$handler = HandlerStack::create( new MockHandler( [ new GuzzleHttp\Exception\RequestException(
|
|
'Connection timed out', new Request( 'GET', $this->exampleUrl )
|
|
) ] ) );
|
|
$r = new GuzzleHttpRequest( $this->exampleUrl,
|
|
[ 'handler' => $handler ] + $this->timeoutOptions );
|
|
$s = $r->execute();
|
|
$errorMsg = $s->getErrorsByType( 'error' )[0]['message'];
|
|
|
|
$this->assertSame( 0, $r->getStatus() );
|
|
$this->assertEquals( 'http-timed-out', $errorMsg );
|
|
}
|
|
|
|
public function testNotFound() {
|
|
$handler = HandlerStack::create( new MockHandler( [ new Response( 404, [
|
|
'status' => '404',
|
|
] ) ] ) );
|
|
$r = new GuzzleHttpRequest( $this->exampleUrl,
|
|
[ 'handler' => $handler ] + $this->timeoutOptions );
|
|
$s = $r->execute();
|
|
$errorMsg = $s->getErrorsByType( 'error' )[0]['message'];
|
|
|
|
$this->assertEquals( 404, $r->getStatus() );
|
|
$this->assertEquals( 'http-bad-status', $errorMsg );
|
|
}
|
|
|
|
/*
|
|
* Test of POST requests header
|
|
*/
|
|
public function testPostBody() {
|
|
$container = [];
|
|
$history = Middleware::history( $container );
|
|
$stack = HandlerStack::create( new MockHandler( [ new Response() ] ) );
|
|
$stack->push( $history );
|
|
$client = new GuzzleHttpRequest( $this->exampleUrl, [
|
|
'method' => 'POST',
|
|
'handler' => $stack,
|
|
'post' => 'key=value',
|
|
] + $this->timeoutOptions );
|
|
$client->execute();
|
|
|
|
$request = $container[0]['request'];
|
|
$this->assertEquals( 'POST', $request->getMethod() );
|
|
$this->assertEquals( 'application/x-www-form-urlencoded',
|
|
$request->getHeader( 'Content-Type' )[0] );
|
|
}
|
|
|
|
/*
|
|
* Test that cookies from CookieJar were sent in the outgoing request.
|
|
*/
|
|
public function testCookieSent() {
|
|
$domain = wfParseUrl( $this->exampleUrl )['host'];
|
|
$expectedCookies = [ 'cookie1' => 'value1', 'anothercookie' => 'secondvalue' ];
|
|
$jar = new CookieJar;
|
|
foreach ( $expectedCookies as $key => $val ) {
|
|
$jar->setCookie( $key, $val, [ 'domain' => $domain ] );
|
|
}
|
|
|
|
$container = [];
|
|
$history = Middleware::history( $container );
|
|
$stack = HandlerStack::create( new MockHandler( [ new Response() ] ) );
|
|
$stack->push( $history );
|
|
$client = new GuzzleHttpRequest( $this->exampleUrl, [
|
|
'method' => 'POST',
|
|
'handler' => $stack,
|
|
'post' => 'key=value',
|
|
] + $this->timeoutOptions );
|
|
$client->setCookieJar( $jar );
|
|
$client->execute();
|
|
|
|
$request = $container[0]['request'];
|
|
$this->assertEquals( [ 'cookie1=value1; anothercookie=secondvalue' ],
|
|
$request->getHeader( 'Cookie' ) );
|
|
}
|
|
|
|
/*
|
|
* Test that cookies returned by HTTP response were added back into the CookieJar.
|
|
*/
|
|
public function testCookieReceived() {
|
|
$handler = HandlerStack::create( new MockHandler( [ new Response( 200, [
|
|
'status' => 200,
|
|
'Set-Cookie' => [ 'cookie1=value1', 'anothercookie=secondvalue' ]
|
|
] ) ] ) );
|
|
$r = new GuzzleHttpRequest( $this->exampleUrl,
|
|
[ 'handler' => $handler ] + $this->timeoutOptions );
|
|
$r->execute();
|
|
|
|
$domain = wfParseUrl( $this->exampleUrl )['host'];
|
|
$this->assertEquals( 'cookie1=value1; anothercookie=secondvalue',
|
|
$r->getCookieJar()->serializeToHttpRequest( '/', $domain ) );
|
|
}
|
|
}
|