Skip to content

Max_duration is not respected when retry_count > 1 (http-client) #46316

@r-martins

Description

@r-martins

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

  1. 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);
  1. Run php -S localhost:8888 in the folder of the file above
  2. 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;
    }
}
  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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions