Skip to content

Commit c5bfb73

Browse files
committed
[HttpClient] Transfer timeout
1 parent a29aff0 commit c5bfb73

File tree

9 files changed

+79
-3
lines changed

9 files changed

+79
-3
lines changed

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
@@ -282,6 +282,10 @@ public function request(string $method, string $url, array $options = []): Respo
282282
$curlopts[file_exists($options['bindto']) ? CURLOPT_UNIX_SOCKET_PATH : CURLOPT_INTERFACE] = $options['bindto'];
283283
}
284284

285+
if (null !== $options['max_duration']) {
286+
$curlopts[CURLOPT_TIMEOUT_MS] = 1000 * $options['max_duration'];
287+
}
288+
285289
$ch = curl_init();
286290

287291
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
@@ -125,6 +125,7 @@ private static function prepareRequest(?string $method, ?string $url, array $opt
125125
$options['headers'] = $headers;
126126
$options['http_version'] = (string) ($options['http_version'] ?? '') ?: null;
127127
$options['timeout'] = (float) ($options['timeout'] ?? ini_get('default_socket_timeout'));
128+
$options['max_duration'] = isset($options['max_duration']) ? (float) $options['max_duration'] : null;
128129

129130
return [$url, $options];
130131
}

src/Symfony/Component/HttpClient/NativeHttpClient.php

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,25 @@ public function request(string $method, string $url, array $options = []): Respo
110110
'debug' => \extension_loaded('curl') ? '' : "* Enable the curl extension for better performance\n",
111111
];
112112

113+
$checkMaxDuration = null;
114+
if (null !== $maxDuration = $options['max_duration']) {
115+
$checkMaxDuration = static function() use ($maxDuration, &$info): void {
116+
if ($info['total_time'] >= $maxDuration) {
117+
throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url'])));
118+
}
119+
};
120+
}
121+
113122
if ($onProgress = $options['on_progress']) {
114123
// Memoize the last progress to ease calling the callback periodically when no network transfer happens
115124
$lastProgress = [0, 0];
116-
$onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info) {
125+
$onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info, $checkMaxDuration) {
126+
if ($checkMaxDuration) {
127+
$info['total_time'] = microtime(true) - $info['start_time'];
128+
129+
$checkMaxDuration();
130+
}
131+
117132
$progressInfo = $info;
118133
$progressInfo['url'] = implode('', $info['url']);
119134
unset($progressInfo['size_body']);
@@ -127,10 +142,12 @@ public function request(string $method, string $url, array $options = []): Respo
127142

128143
$onProgress($lastProgress[0], $lastProgress[1], $progressInfo);
129144
};
145+
} elseif ($checkMaxDuration) {
146+
$onProgress = $checkMaxDuration;
130147
}
131148

132149
// Always register a notification callback to compute live stats about the response
133-
$notification = static function (int $code, int $severity, ?string $msg, int $msgCode, int $dlNow, int $dlSize) use ($onProgress, &$info) {
150+
$notification = static function (int $code, int $severity, ?string $msg, int $msgCode, int $dlNow, int $dlSize) use ($onProgress, &$info, $checkMaxDuration) {
134151
$info['total_time'] = microtime(true) - $info['start_time'];
135152

136153
if (STREAM_NOTIFY_PROGRESS === $code) {
@@ -142,6 +159,10 @@ public function request(string $method, string $url, array $options = []): Respo
142159
$info['debug'] .= $info['request_header'];
143160
unset($info['request_header']);
144161
} else {
162+
if ($checkMaxDuration) {
163+
$checkMaxDuration();
164+
}
165+
145166
return;
146167
}
147168

@@ -166,6 +187,10 @@ public function request(string $method, string $url, array $options = []): Respo
166187
$options['request_headers'][] = 'user-agent: Symfony HttpClient/Native';
167188
}
168189

190+
if (null !== $options['max_duration']) {
191+
$options['timeout'] = min($options['max_duration'], $options['timeout']);
192+
}
193+
169194
$context = [
170195
'http' => [
171196
'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.5",
2626
"symfony/polyfill-php73": "^1.11"
2727
},
2828
"require-dev": {

src/Symfony/Contracts/HttpClient/HttpClientInterface.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ 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 inactivity timeout - defaults to ini_get('default_socket_timeout')
56+
'max_duration' => null, // float - the maximum execution time of the whole request (including the connection time)
5657
'bindto' => '0', // string - the interface or the local socket to bind to
5758
'verify_peer' => true, // see https://php.net/context.ssl for the following options
5859
'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
@@ -778,4 +778,25 @@ public function testGzipBroken()
778778
$this->expectException(TransportExceptionInterface::class);
779779
$response->getContent();
780780
}
781+
782+
public function testMaxDuration()
783+
{
784+
$client = $this->getHttpClient(__FUNCTION__);
785+
$response = $client->request('GET', 'http://localhost:8057/max-duration', [
786+
'max_duration' => 0.1,
787+
]);
788+
789+
$start = microtime(true);
790+
791+
try {
792+
$response->getContent();
793+
} catch (TransportExceptionInterface $e) {
794+
$this->addToAssertionCount(1);
795+
}
796+
797+
$duration = microtime(true) - $start;
798+
799+
$this->assertGreaterThanOrEqual(0.1, $duration);
800+
$this->assertLessThan(0.2, $duration);
801+
}
781802
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy