Skip to content

Commit

Permalink
feature #32807 [HttpClient] add "max_duration" option (fancyweb)
Browse files Browse the repository at this point in the history
This PR was merged into the 4.4 branch.

Discussion
----------

[HttpClient] add "max_duration" option

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #32765
| License       | MIT
| Doc PR        | symfony/symfony-docs#12073

Commits
-------

a4178f1 [HttpClient] add "max_duration" option
  • Loading branch information
nicolas-grekas committed Aug 6, 2019
2 parents 405c64c + a4178f1 commit 3498259
Show file tree
Hide file tree
Showing 15 changed files with 81 additions and 2 deletions.
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('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 @@ -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 @@ -777,4 +777,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);
}
}

0 comments on commit 3498259

Please sign in to comment.