Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3498259

Browse files
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 | #32765 | License | MIT | Doc PR | symfony/symfony-docs#12073 Commits ------- a4178f1 [HttpClient] add "max_duration" option
2 parents 405c64c + a4178f1 commit 3498259

File tree

15 files changed

+81
-2
lines changed

15 files changed

+81
-2
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1367,6 +1367,9 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode)
13671367
->floatNode('timeout')
13681368
->info('The idle timeout, defaults to the "default_socket_timeout" ini parameter.')
13691369
->end()
1370+
->floatNode('max_duration')
1371+
->info('The maximum execution time for the request+response as a whole.')
1372+
->end()
13701373
->scalarNode('bindto')
13711374
->info('A network interface name, IP address, a host name or a UNIX socket to bind to.')
13721375
->end()
@@ -1503,6 +1506,9 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode)
15031506
->floatNode('timeout')
15041507
->info('The idle timeout, defaults to the "default_socket_timeout" ini parameter.')
15051508
->end()
1509+
->floatNode('max_duration')
1510+
->info('The maximum execution time for the request+response as a whole.')
1511+
->end()
15061512
->scalarNode('bindto')
15071513
->info('A network interface name, IP address, a host name or a UNIX socket to bind to.')
15081514
->end()

src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@
495495
<xsd:attribute name="proxy" type="xsd:string" />
496496
<xsd:attribute name="no-proxy" type="xsd:string" />
497497
<xsd:attribute name="timeout" type="xsd:float" />
498+
<xsd:attribute name="max-duration" type="xsd:float" />
498499
<xsd:attribute name="bindto" type="xsd:string" />
499500
<xsd:attribute name="verify-peer" type="xsd:boolean" />
500501
<xsd:attribute name="verify-host" type="xsd:boolean" />

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_full_default_options.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
'resolve' => ['localhost' => '127.0.0.1'],
1010
'proxy' => 'proxy.org',
1111
'timeout' => 3.5,
12+
'max_duration' => 10.1,
1213
'bindto' => '127.0.0.1',
1314
'verify_peer' => true,
1415
'verify_host' => true,

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_full_default_options.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
proxy="proxy.org"
1212
bindto="127.0.0.1"
1313
timeout="3.5"
14+
max-duration="10.1"
1415
verify-peer="true"
1516
max-redirects="2"
1617
http-version="2.0"

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_full_default_options.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ framework:
88
resolve: {'localhost': '127.0.0.1'}
99
proxy: proxy.org
1010
timeout: 3.5
11+
max_duration: 10.1
1112
bindto: 127.0.0.1
1213
verify_peer: true
1314
verify_host: true

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1547,6 +1547,7 @@ public function testHttpClientFullDefaultOptions()
15471547
$this->assertSame(['localhost' => '127.0.0.1'], $defaultOptions['resolve']);
15481548
$this->assertSame('proxy.org', $defaultOptions['proxy']);
15491549
$this->assertSame(3.5, $defaultOptions['timeout']);
1550+
$this->assertSame(10.1, $defaultOptions['max_duration']);
15501551
$this->assertSame('127.0.0.1', $defaultOptions['bindto']);
15511552
$this->assertTrue($defaultOptions['verify_peer']);
15521553
$this->assertTrue($defaultOptions['verify_host']);

src/Symfony/Component/HttpClient/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ CHANGELOG
99
* added support for NTLM authentication
1010
* added `$response->toStream()` to cast responses to regular PHP streams
1111
* made `Psr18Client` implement relevant PSR-17 factories and have streaming responses
12+
* added `max_duration` option
1213

1314
4.3.0
1415
-----

src/Symfony/Component/HttpClient/CurlHttpClient.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,10 @@ public function request(string $method, string $url, array $options = []): Respo
284284
$curlopts[file_exists($options['bindto']) ? CURLOPT_UNIX_SOCKET_PATH : CURLOPT_INTERFACE] = $options['bindto'];
285285
}
286286

287+
if (0 < $options['max_duration']) {
288+
$curlopts[CURLOPT_TIMEOUT_MS] = 1000 * $options['max_duration'];
289+
}
290+
287291
$ch = curl_init();
288292

289293
foreach ($curlopts as $opt => $value) {

src/Symfony/Component/HttpClient/HttpClientTrait.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ private static function prepareRequest(?string $method, ?string $url, array $opt
113113
// Finalize normalization of options
114114
$options['http_version'] = (string) ($options['http_version'] ?? '') ?: null;
115115
$options['timeout'] = (float) ($options['timeout'] ?? ini_get('default_socket_timeout'));
116+
$options['max_duration'] = isset($options['max_duration']) ? (float) $options['max_duration'] : 0;
116117

117118
return [$url, $options];
118119
}

src/Symfony/Component/HttpClient/NativeHttpClient.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,12 @@ public function request(string $method, string $url, array $options = []): Respo
113113
if ($onProgress = $options['on_progress']) {
114114
// Memoize the last progress to ease calling the callback periodically when no network transfer happens
115115
$lastProgress = [0, 0];
116-
$onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info) {
116+
$maxDuration = 0 < $options['max_duration'] ? $options['max_duration'] : INF;
117+
$onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info, $maxDuration) {
118+
if ($info['total_time'] >= $maxDuration) {
119+
throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url'])));
120+
}
121+
117122
$progressInfo = $info;
118123
$progressInfo['url'] = implode('', $info['url']);
119124
unset($progressInfo['size_body']);
@@ -127,6 +132,13 @@ public function request(string $method, string $url, array $options = []): Respo
127132

128133
$onProgress($lastProgress[0], $lastProgress[1], $progressInfo);
129134
};
135+
} elseif (0 < $options['max_duration']) {
136+
$maxDuration = $options['max_duration'];
137+
$onProgress = static function () use (&$info, $maxDuration): void {
138+
if ($info['total_time'] >= $maxDuration) {
139+
throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url'])));
140+
}
141+
};
130142
}
131143

132144
// Always register a notification callback to compute live stats about the response
@@ -166,6 +178,10 @@ public function request(string $method, string $url, array $options = []): Respo
166178
$options['headers'][] = 'User-Agent: Symfony HttpClient/Native';
167179
}
168180

181+
if (0 < $options['max_duration']) {
182+
$options['timeout'] = min($options['max_duration'], $options['timeout']);
183+
}
184+
169185
$context = [
170186
'http' => [
171187
'protocol_version' => $options['http_version'] ?: '1.1',

src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,19 @@ protected function getHttpClient(string $testCase): HttpClientInterface
123123
$body = ['<1>', '', '<2>'];
124124
$responses[] = new MockResponse($body, ['response_headers' => $headers]);
125125
break;
126+
127+
case 'testMaxDuration':
128+
$mock = $this->getMockBuilder(ResponseInterface::class)->getMock();
129+
$mock->expects($this->any())
130+
->method('getContent')
131+
->willReturnCallback(static function (): void {
132+
usleep(100000);
133+
134+
throw new TransportException('Max duration was reached.');
135+
});
136+
137+
$responses[] = $mock;
138+
break;
126139
}
127140

128141
return new MockHttpClient($responses);

src/Symfony/Component/HttpClient/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"require": {
2323
"php": "^7.1.3",
2424
"psr/log": "^1.0",
25-
"symfony/http-client-contracts": "^1.1.4",
25+
"symfony/http-client-contracts": "^1.1.6",
2626
"symfony/polyfill-php73": "^1.11"
2727
},
2828
"require-dev": {

src/Symfony/Contracts/HttpClient/HttpClientInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ interface HttpClientInterface
5353
'proxy' => null, // string - by default, the proxy-related env vars handled by curl SHOULD be honored
5454
'no_proxy' => null, // string - a comma separated list of hosts that do not require a proxy to be reached
5555
'timeout' => null, // float - the idle timeout - defaults to ini_get('default_socket_timeout')
56+
'max_duration' => 0, // float - the maximum execution time for the request+response as a whole;
57+
// a value lower than or equal to 0 means it is unlimited
5658
'bindto' => '0', // string - the interface or the local socket to bind to
5759
'verify_peer' => true, // see https://php.net/context.ssl for the following options
5860
'verify_host' => true,

src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,16 @@
132132
header('Content-Encoding: gzip');
133133
echo str_repeat('-', 1000);
134134
exit;
135+
136+
case '/max-duration':
137+
ignore_user_abort(false);
138+
while (true) {
139+
echo '<1>';
140+
@ob_flush();
141+
flush();
142+
usleep(500);
143+
}
144+
exit;
135145
}
136146

137147
header('Content-Type: application/json', true);

src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,4 +777,25 @@ public function testGzipBroken()
777777
$this->expectException(TransportExceptionInterface::class);
778778
$response->getContent();
779779
}
780+
781+
public function testMaxDuration()
782+
{
783+
$client = $this->getHttpClient(__FUNCTION__);
784+
$response = $client->request('GET', 'http://localhost:8057/max-duration', [
785+
'max_duration' => 0.1,
786+
]);
787+
788+
$start = microtime(true);
789+
790+
try {
791+
$response->getContent();
792+
} catch (TransportExceptionInterface $e) {
793+
$this->addToAssertionCount(1);
794+
}
795+
796+
$duration = microtime(true) - $start;
797+
798+
$this->assertGreaterThanOrEqual(0.1, $duration);
799+
$this->assertLessThan(0.2, $duration);
800+
}
780801
}

0 commit comments

Comments
 (0)