feature #32807 [HttpClient] add "max_duration" option (fancyweb)
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 | https://github.com/symfony/symfony/issues/32765
| License | MIT
| Doc PR | https://github.com/symfony/symfony-docs/pull/12073
Commits
-------
a4178f1369
[HttpClient] add "max_duration" option
This commit is contained in:
commit
3498259b12
@ -1367,6 +1367,9 @@ class Configuration implements ConfigurationInterface
|
|||||||
->floatNode('timeout')
|
->floatNode('timeout')
|
||||||
->info('The idle timeout, defaults to the "default_socket_timeout" ini parameter.')
|
->info('The idle timeout, defaults to the "default_socket_timeout" ini parameter.')
|
||||||
->end()
|
->end()
|
||||||
|
->floatNode('max_duration')
|
||||||
|
->info('The maximum execution time for the request+response as a whole.')
|
||||||
|
->end()
|
||||||
->scalarNode('bindto')
|
->scalarNode('bindto')
|
||||||
->info('A network interface name, IP address, a host name or a UNIX socket to bind to.')
|
->info('A network interface name, IP address, a host name or a UNIX socket to bind to.')
|
||||||
->end()
|
->end()
|
||||||
@ -1503,6 +1506,9 @@ class Configuration implements ConfigurationInterface
|
|||||||
->floatNode('timeout')
|
->floatNode('timeout')
|
||||||
->info('The idle timeout, defaults to the "default_socket_timeout" ini parameter.')
|
->info('The idle timeout, defaults to the "default_socket_timeout" ini parameter.')
|
||||||
->end()
|
->end()
|
||||||
|
->floatNode('max_duration')
|
||||||
|
->info('The maximum execution time for the request+response as a whole.')
|
||||||
|
->end()
|
||||||
->scalarNode('bindto')
|
->scalarNode('bindto')
|
||||||
->info('A network interface name, IP address, a host name or a UNIX socket to bind to.')
|
->info('A network interface name, IP address, a host name or a UNIX socket to bind to.')
|
||||||
->end()
|
->end()
|
||||||
|
@ -495,6 +495,7 @@
|
|||||||
<xsd:attribute name="proxy" type="xsd:string" />
|
<xsd:attribute name="proxy" type="xsd:string" />
|
||||||
<xsd:attribute name="no-proxy" type="xsd:string" />
|
<xsd:attribute name="no-proxy" type="xsd:string" />
|
||||||
<xsd:attribute name="timeout" type="xsd:float" />
|
<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="bindto" type="xsd:string" />
|
||||||
<xsd:attribute name="verify-peer" type="xsd:boolean" />
|
<xsd:attribute name="verify-peer" type="xsd:boolean" />
|
||||||
<xsd:attribute name="verify-host" type="xsd:boolean" />
|
<xsd:attribute name="verify-host" type="xsd:boolean" />
|
||||||
|
@ -9,6 +9,7 @@ $container->loadFromExtension('framework', [
|
|||||||
'resolve' => ['localhost' => '127.0.0.1'],
|
'resolve' => ['localhost' => '127.0.0.1'],
|
||||||
'proxy' => 'proxy.org',
|
'proxy' => 'proxy.org',
|
||||||
'timeout' => 3.5,
|
'timeout' => 3.5,
|
||||||
|
'max_duration' => 10.1,
|
||||||
'bindto' => '127.0.0.1',
|
'bindto' => '127.0.0.1',
|
||||||
'verify_peer' => true,
|
'verify_peer' => true,
|
||||||
'verify_host' => true,
|
'verify_host' => true,
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
proxy="proxy.org"
|
proxy="proxy.org"
|
||||||
bindto="127.0.0.1"
|
bindto="127.0.0.1"
|
||||||
timeout="3.5"
|
timeout="3.5"
|
||||||
|
max-duration="10.1"
|
||||||
verify-peer="true"
|
verify-peer="true"
|
||||||
max-redirects="2"
|
max-redirects="2"
|
||||||
http-version="2.0"
|
http-version="2.0"
|
||||||
|
@ -8,6 +8,7 @@ framework:
|
|||||||
resolve: {'localhost': '127.0.0.1'}
|
resolve: {'localhost': '127.0.0.1'}
|
||||||
proxy: proxy.org
|
proxy: proxy.org
|
||||||
timeout: 3.5
|
timeout: 3.5
|
||||||
|
max_duration: 10.1
|
||||||
bindto: 127.0.0.1
|
bindto: 127.0.0.1
|
||||||
verify_peer: true
|
verify_peer: true
|
||||||
verify_host: true
|
verify_host: true
|
||||||
|
@ -1547,6 +1547,7 @@ abstract class FrameworkExtensionTest extends TestCase
|
|||||||
$this->assertSame(['localhost' => '127.0.0.1'], $defaultOptions['resolve']);
|
$this->assertSame(['localhost' => '127.0.0.1'], $defaultOptions['resolve']);
|
||||||
$this->assertSame('proxy.org', $defaultOptions['proxy']);
|
$this->assertSame('proxy.org', $defaultOptions['proxy']);
|
||||||
$this->assertSame(3.5, $defaultOptions['timeout']);
|
$this->assertSame(3.5, $defaultOptions['timeout']);
|
||||||
|
$this->assertSame(10.1, $defaultOptions['max_duration']);
|
||||||
$this->assertSame('127.0.0.1', $defaultOptions['bindto']);
|
$this->assertSame('127.0.0.1', $defaultOptions['bindto']);
|
||||||
$this->assertTrue($defaultOptions['verify_peer']);
|
$this->assertTrue($defaultOptions['verify_peer']);
|
||||||
$this->assertTrue($defaultOptions['verify_host']);
|
$this->assertTrue($defaultOptions['verify_host']);
|
||||||
|
@ -9,6 +9,7 @@ CHANGELOG
|
|||||||
* added support for NTLM authentication
|
* added support for NTLM authentication
|
||||||
* added `$response->toStream()` to cast responses to regular PHP streams
|
* added `$response->toStream()` to cast responses to regular PHP streams
|
||||||
* made `Psr18Client` implement relevant PSR-17 factories and have streaming responses
|
* made `Psr18Client` implement relevant PSR-17 factories and have streaming responses
|
||||||
|
* added `max_duration` option
|
||||||
|
|
||||||
4.3.0
|
4.3.0
|
||||||
-----
|
-----
|
||||||
|
@ -284,6 +284,10 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
|
|||||||
$curlopts[file_exists($options['bindto']) ? CURLOPT_UNIX_SOCKET_PATH : CURLOPT_INTERFACE] = $options['bindto'];
|
$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();
|
$ch = curl_init();
|
||||||
|
|
||||||
foreach ($curlopts as $opt => $value) {
|
foreach ($curlopts as $opt => $value) {
|
||||||
|
@ -113,6 +113,7 @@ trait HttpClientTrait
|
|||||||
// Finalize normalization of options
|
// Finalize normalization of options
|
||||||
$options['http_version'] = (string) ($options['http_version'] ?? '') ?: null;
|
$options['http_version'] = (string) ($options['http_version'] ?? '') ?: null;
|
||||||
$options['timeout'] = (float) ($options['timeout'] ?? ini_get('default_socket_timeout'));
|
$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];
|
return [$url, $options];
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,12 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
|
|||||||
if ($onProgress = $options['on_progress']) {
|
if ($onProgress = $options['on_progress']) {
|
||||||
// Memoize the last progress to ease calling the callback periodically when no network transfer happens
|
// Memoize the last progress to ease calling the callback periodically when no network transfer happens
|
||||||
$lastProgress = [0, 0];
|
$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 = $info;
|
||||||
$progressInfo['url'] = implode('', $info['url']);
|
$progressInfo['url'] = implode('', $info['url']);
|
||||||
unset($progressInfo['size_body']);
|
unset($progressInfo['size_body']);
|
||||||
@ -127,6 +132,13 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
|
|||||||
|
|
||||||
$onProgress($lastProgress[0], $lastProgress[1], $progressInfo);
|
$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
|
// Always register a notification callback to compute live stats about the response
|
||||||
@ -166,6 +178,10 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
|
|||||||
$options['headers'][] = 'User-Agent: Symfony HttpClient/Native';
|
$options['headers'][] = 'User-Agent: Symfony HttpClient/Native';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (0 < $options['max_duration']) {
|
||||||
|
$options['timeout'] = min($options['max_duration'], $options['timeout']);
|
||||||
|
}
|
||||||
|
|
||||||
$context = [
|
$context = [
|
||||||
'http' => [
|
'http' => [
|
||||||
'protocol_version' => $options['http_version'] ?: '1.1',
|
'protocol_version' => $options['http_version'] ?: '1.1',
|
||||||
|
@ -123,6 +123,19 @@ class MockHttpClientTest extends HttpClientTestCase
|
|||||||
$body = ['<1>', '', '<2>'];
|
$body = ['<1>', '', '<2>'];
|
||||||
$responses[] = new MockResponse($body, ['response_headers' => $headers]);
|
$responses[] = new MockResponse($body, ['response_headers' => $headers]);
|
||||||
break;
|
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);
|
return new MockHttpClient($responses);
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"php": "^7.1.3",
|
"php": "^7.1.3",
|
||||||
"psr/log": "^1.0",
|
"psr/log": "^1.0",
|
||||||
"symfony/http-client-contracts": "^1.1.4",
|
"symfony/http-client-contracts": "^1.1.6",
|
||||||
"symfony/polyfill-php73": "^1.11"
|
"symfony/polyfill-php73": "^1.11"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
@ -53,6 +53,8 @@ interface HttpClientInterface
|
|||||||
'proxy' => null, // string - by default, the proxy-related env vars handled by curl SHOULD be honored
|
'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
|
'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')
|
'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
|
'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_peer' => true, // see https://php.net/context.ssl for the following options
|
||||||
'verify_host' => true,
|
'verify_host' => true,
|
||||||
|
@ -132,6 +132,16 @@ switch ($vars['REQUEST_URI']) {
|
|||||||
header('Content-Encoding: gzip');
|
header('Content-Encoding: gzip');
|
||||||
echo str_repeat('-', 1000);
|
echo str_repeat('-', 1000);
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
|
case '/max-duration':
|
||||||
|
ignore_user_abort(false);
|
||||||
|
while (true) {
|
||||||
|
echo '<1>';
|
||||||
|
@ob_flush();
|
||||||
|
flush();
|
||||||
|
usleep(500);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
header('Content-Type: application/json', true);
|
header('Content-Type: application/json', true);
|
||||||
|
@ -777,4 +777,25 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
$this->expectException(TransportExceptionInterface::class);
|
$this->expectException(TransportExceptionInterface::class);
|
||||||
$response->getContent();
|
$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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user