Description
Symfony version(s) affected
5.4.8
Description
Let's say you have an API endpoint that always fails on the first request, and succeed on the second attempt.
Every request takes 30 seconds to be returned by the API.
If you make a Retryable HttpRequest, you can define timeout
and max_duration
to 40 seconds.
But it will not be respected, because when doing the second attempt, symfony still waits up to 40 seconds to get a response.
So you can expect this part of code to take 60 seconds to respond with no issues, instead of having a timeout error.
How to reproduce
- Create the following
sleep.php
file:
<?php
sleep(30);
$count = (int)@file_get_contents('.count') == 0 ? 1 : (int)@file_get_contents('.count');
if ($count >= 2) {
unlink('.count');
echo '2nd attempt. SUCCESS!';
exit;
}
$count++;
http_response_code(502);
echo 'Failed. ' . $count-1 . ' attempt;';
file_put_contents('.count', (string)$count);
- Run
php -S localhost:8888
in the folder of the file above - Create a Command to test it:
<?php
#src/TestCommand.php
namespace App;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class TestCommand extends \Symfony\Component\Console\Command\Command
{
protected static $defaultName = 'app:test';
public function __construct(protected HttpClientInterface $httpClient, string $name = null)
{
parent::__construct($name);
}
protected function configure()
{
parent::configure(); // TODO: Change the autogenerated stub
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Start: ' . date('H:i:s'));
$start = time();
$url = 'http://localhost:8888/sleep.php';
$client = $this->httpClient;
$response = $client->request('GET', $url, [
'timeout' => 40,
'max_duration' => 40
]);
try {
$output->writeln($response->getStatusCode().': '.$response->getContent(false));
} catch (ClientExceptionInterface $e) {
$output->writeln(get_class($e) . ': ' . $e->getMessage());
} catch (RedirectionExceptionInterface $e) {
$output->writeln(get_class($e) . ': ' . $e->getMessage());
} catch (ServerExceptionInterface $e) {
$output->writeln(get_class($e) . ': ' . $e->getMessage());
} catch (TransportExceptionInterface $e) {
$output->writeln(get_class($e) . ': ' . $e->getMessage());
}
$output->writeln('Retry coint: ' . (int)$response->getInfo('retry_count'));
$output->writeln('End: ' . date('H:i:s'));
$output->writeln('Total time: ' . time()-$start . 's');
return 1;
}
}
- Run the test comand with
bin/console app:test
and note that it will succeed on the second attempt after 60 seconds.
Possible Solution
We may need to work on AmpHttpClient or RetryableHttpClient to use the time left on the other requests.
I posted a half solution here that may be useful to get the total time of the request, including the waiting time between the attempts.
But it's far from the ideal solution.
Additional Context
In my case, I have a controller that makes http requests to another endpoint.
The users that call this controller, expect it to return a response in 45 seconds. However, after implementing the RetryableHttpClient, I'm having cases where the request is processed by the other endpoint in the second or third attempt, but my users get a timeout exception.
It happens because the first request to the other endpoint takes less than 45 seconds, but both calls take more than that.