Skip to content

[HttpClient] RetryableHttpClient does not call retry strategy when it's decorated with client that returns AsyncResponse #58050

@Tetragramat

Description

@Tetragramat

Symfony version(s) affected

5.4.42

Description

On timeout RetryableHttpClient does not call retry strategy when RetryableHttpClient is decorated with client that returns AsyncResponse.

If I reverse decoration order then it works.
If I just return parent response then it works too.

I need to decorate client with AsyncResponse and passthru so I can log requests and responses.

it's not working on both 5.4.42 and 6.4.10.

How to reproduce

<?php

declare(strict_types=1);

namespace App\Tests;

use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\AsyncDecoratorTrait;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Response\AsyncContext;
use Symfony\Component\HttpClient\Response\AsyncResponse;
use Symfony\Component\HttpClient\Retry\RetryStrategyInterface;
use Symfony\Component\HttpClient\RetryableHttpClient;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\Test\TestHttpServer;

class UnitTest extends TestCase
{
	public function testIssue()
	{
		$strategy = new class() implements RetryStrategyInterface {
			public bool $isCalled = false;

			public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool
			{
				$this->isCalled = true; // this should always get called

				return false;
			}

			public function getDelay(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): int
			{
				return 0;
			}
		};

		$client = HttpClient::create();

		$client = new RetryableHttpClient(
			client: $client,
			strategy: $strategy,
		);

		// if this is removed or moved above RetryableHttpClient, then it works as expected
		$client = new class($client) implements HttpClientInterface
		{
			use AsyncDecoratorTrait;

			public function request(string $method, string $url, array $options = []): ResponseInterface
			{
				return new AsyncResponse($this->client, $method, $url, $options);
			}
		};

		TestHttpServer::start();

		/** @see vendor/symfony/http-client-contracts/Test/Fixtures/web/index.php */
		$response = $client->request('GET', 'http://localhost:8057/timeout-header', ['timeout' => 0.1]);

		try {
			$response->getStatusCode();

			self::fail();
		} catch (TransportExceptionInterface $e) {
			self::assertSame('Idle timeout reached for "http://localhost:8057/timeout-header".', $e->getMessage());
			self::assertTrue($strategy->isCalled);
		}
	}
}

Possible Solution

No response

Additional Context

No response

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