Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[HttpClient] add "max_duration" option #32807

Merged
merged 1 commit into from
Aug 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -1367,6 +1367,9 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode)
->floatNode('timeout')
->info('The idle timeout, defaults to the "default_socket_timeout" ini parameter.')
->end()
->floatNode('max_duration')
->info('The maximum execution time for the request+response as a whole.')
->end()
->scalarNode('bindto')
->info('A network interface name, IP address, a host name or a UNIX socket to bind to.')
->end()
Expand Down Expand Up @@ -1503,6 +1506,9 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode)
->floatNode('timeout')
->info('Defaults to "default_socket_timeout" ini parameter.')
->end()
->floatNode('max_duration')
->info('The maximum execution time for the request+response as a whole.')
->end()
->scalarNode('bindto')
->info('A network interface name, IP address, a host name or a UNIX socket to bind to.')
->end()
Expand Down
Expand Up @@ -495,6 +495,7 @@
<xsd:attribute name="proxy" type="xsd:string" />
<xsd:attribute name="no-proxy" type="xsd:string" />
<xsd:attribute name="timeout" type="xsd:float" />
<xsd:attribute name="max-duration" type="xsd:float" />
<xsd:attribute name="bindto" type="xsd:string" />
<xsd:attribute name="verify-peer" type="xsd:boolean" />
<xsd:attribute name="verify-host" type="xsd:boolean" />
Expand Down
Expand Up @@ -9,6 +9,7 @@
'resolve' => ['localhost' => '127.0.0.1'],
'proxy' => 'proxy.org',
'timeout' => 3.5,
'max_duration' => 10.1,
'bindto' => '127.0.0.1',
'verify_peer' => true,
'verify_host' => true,
Expand Down
Expand Up @@ -11,6 +11,7 @@
proxy="proxy.org"
bindto="127.0.0.1"
timeout="3.5"
max-duration="10.1"
verify-peer="true"
max-redirects="2"
http-version="2.0"
Expand Down
Expand Up @@ -8,6 +8,7 @@ framework:
resolve: {'localhost': '127.0.0.1'}
proxy: proxy.org
timeout: 3.5
max_duration: 10.1
bindto: 127.0.0.1
verify_peer: true
verify_host: true
Expand Down
Expand Up @@ -1547,6 +1547,7 @@ public function testHttpClientFullDefaultOptions()
$this->assertSame(['localhost' => '127.0.0.1'], $defaultOptions['resolve']);
$this->assertSame('proxy.org', $defaultOptions['proxy']);
$this->assertSame(3.5, $defaultOptions['timeout']);
$this->assertSame(10.1, $defaultOptions['max_duration']);
$this->assertSame('127.0.0.1', $defaultOptions['bindto']);
$this->assertTrue($defaultOptions['verify_peer']);
$this->assertTrue($defaultOptions['verify_host']);
Expand Down
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 @@ -284,6 +284,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 @@ -113,6 +113,7 @@ private static function prepareRequest(?string $method, ?string $url, array $opt
// Finalize normalization of options
$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['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 idle 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);
}
}