Skip to content

Commit

Permalink
[HttpClient] Transfer timeout
Browse files Browse the repository at this point in the history
  • Loading branch information
fancyweb committed Jul 31, 2019
1 parent 40fe161 commit bc7bead
Show file tree
Hide file tree
Showing 10 changed files with 71 additions and 3 deletions.
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpClient/CHANGELOG.md
Expand Up @@ -9,6 +9,7 @@ CHANGELOG
* added support for NTLM authentication
* added `$response->toStream()` to cast responses to regular PHP streams
* made `Psr18Client` implement relevant PSR-17 factories and have streaming responses
* added `max_duration` option

4.3.0
-----
Expand Down
4 changes: 4 additions & 0 deletions src/Symfony/Component/HttpClient/CurlHttpClient.php
Expand Up @@ -282,6 +282,10 @@ public function request(string $method, string $url, array $options = []): Respo
$curlopts[file_exists($options['bindto']) ? CURLOPT_UNIX_SOCKET_PATH : CURLOPT_INTERFACE] = $options['bindto'];
}

if (0 < $options['max_duration']) {
$curlopts[CURLOPT_TIMEOUT_MS] = 1000 * $options['max_duration'];
}

$ch = curl_init();

foreach ($curlopts as $opt => $value) {
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpClient/HttpClientTrait.php
Expand Up @@ -125,6 +125,7 @@ private static function prepareRequest(?string $method, ?string $url, array $opt
$options['headers'] = $headers;
$options['http_version'] = (string) ($options['http_version'] ?? '') ?: null;
$options['timeout'] = (float) ($options['timeout'] ?? ini_get('default_socket_timeout'));
$options['max_duration'] = isset($options['max_duration']) ? (float) $options['max_duration'] : 0;

return [$url, $options];
}
Expand Down
18 changes: 17 additions & 1 deletion src/Symfony/Component/HttpClient/NativeHttpClient.php
Expand Up @@ -113,7 +113,12 @@ public function request(string $method, string $url, array $options = []): Respo
if ($onProgress = $options['on_progress']) {
// Memoize the last progress to ease calling the callback periodically when no network transfer happens
$lastProgress = [0, 0];
$onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info) {
$maxDuration = 0 < $options['max_duration'] ? $options['max_duration'] : INF;
$onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info, $maxDuration) {
if ($info['total_time'] >= $maxDuration) {
throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url'])));
}

$progressInfo = $info;
$progressInfo['url'] = implode('', $info['url']);
unset($progressInfo['size_body']);
Expand All @@ -127,6 +132,13 @@ public function request(string $method, string $url, array $options = []): Respo

$onProgress($lastProgress[0], $lastProgress[1], $progressInfo);
};
} elseif (0 < $options['max_duration']) {
$maxDuration = $options['max_duration'];
$onProgress = static function () use (&$info, $maxDuration): void {
if ($info['total_time'] >= $maxDuration) {
throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url'])));
}
};
}

// Always register a notification callback to compute live stats about the response
Expand Down Expand Up @@ -166,6 +178,10 @@ public function request(string $method, string $url, array $options = []): Respo
$options['request_headers'][] = 'user-agent: Symfony HttpClient/Native';
}

if (0 < $options['max_duration']) {
$options['timeout'] = min($options['max_duration'], $options['timeout']);
}

$context = [
'http' => [
'protocol_version' => $options['http_version'] ?: '1.1',
Expand Down
13 changes: 13 additions & 0 deletions src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php
Expand Up @@ -123,6 +123,19 @@ protected function getHttpClient(string $testCase): HttpClientInterface
$body = ['<1>', '', '<2>'];
$responses[] = new MockResponse($body, ['response_headers' => $headers]);
break;

case 'testMaxDuration':
$mock = $this->getMockBuilder(ResponseInterface::class)->getMock();
$mock->expects($this->any())
->method('getContent')
->willReturnCallback(static function (): void {
usleep(100000);

throw new TransportException('Max duration was reached.');
});

$responses[] = $mock;
break;
}

return new MockHttpClient($responses);
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/HttpClient/composer.json
Expand Up @@ -22,7 +22,7 @@
"require": {
"php": "^7.1.3",
"psr/log": "^1.0",
"symfony/http-client-contracts": "^1.1.4",
"symfony/http-client-contracts": "^1.1.6",
"symfony/polyfill-php73": "^1.11"
},
"require-dev": {
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Contracts/HttpClient/HttpClientInterface.php
Expand Up @@ -53,6 +53,8 @@ interface HttpClientInterface
'proxy' => null, // string - by default, the proxy-related env vars handled by curl SHOULD be honored
'no_proxy' => null, // string - a comma separated list of hosts that do not require a proxy to be reached
'timeout' => null, // float - the inactivity timeout - defaults to ini_get('default_socket_timeout')
'max_duration' => 0, // float - the maximum execution time for the request+response as a whole;
// a value lower than or equal to 0 means it is unlimited
'bindto' => '0', // string - the interface or the local socket to bind to
'verify_peer' => true, // see https://php.net/context.ssl for the following options
'verify_host' => true,
Expand Down
10 changes: 10 additions & 0 deletions src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php
Expand Up @@ -132,6 +132,16 @@
header('Content-Encoding: gzip');
echo str_repeat('-', 1000);
exit;

case '/max-duration':
ignore_user_abort(false);
while (true) {
echo '<1>';
@ob_flush();
flush();
usleep(500);
}
exit;
}

header('Content-Type: application/json', true);
Expand Down
21 changes: 21 additions & 0 deletions src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php
Expand Up @@ -778,4 +778,25 @@ public function testGzipBroken()
$this->expectException(TransportExceptionInterface::class);
$response->getContent();
}

public function testMaxDuration()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/max-duration', [
'max_duration' => 0.1,
]);

$start = microtime(true);

try {
$response->getContent();
} catch (TransportExceptionInterface $e) {
$this->addToAssertionCount(1);
}

$duration = microtime(true) - $start;

$this->assertGreaterThanOrEqual(0.1, $duration);
$this->assertLessThan(0.2, $duration);
}
}
2 changes: 1 addition & 1 deletion src/Symfony/Contracts/HttpClient/composer.json
Expand Up @@ -27,7 +27,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "1.2-dev"
"dev-master": "1.1-dev"
}
}
}

0 comments on commit bc7bead

Please sign in to comment.