diff --git a/src/Symfony/Component/HttpClient/HttplugClient.php b/src/Symfony/Component/HttpClient/HttplugClient.php index ec00b3234a037..5d691e024d47c 100644 --- a/src/Symfony/Component/HttpClient/HttplugClient.php +++ b/src/Symfony/Component/HttpClient/HttplugClient.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpClient; use GuzzleHttp\Promise\Promise as GuzzlePromise; +use GuzzleHttp\Promise\RejectedPromise; use Http\Client\Exception\NetworkException; use Http\Client\Exception\RequestException; use Http\Client\HttpAsyncClient; @@ -22,7 +23,6 @@ use Http\Message\StreamFactory; use Http\Message\UriFactory; use Http\Promise\Promise; -use Http\Promise\RejectedPromise; use Nyholm\Psr7\Factory\Psr17Factory; use Nyholm\Psr7\Request; use Nyholm\Psr7\Uri; @@ -114,7 +114,7 @@ public function sendAsyncRequest(RequestInterface $request): Promise try { $response = $this->sendPsr7Request($request, true); } catch (NetworkException $e) { - return new RejectedPromise($e); + return new HttplugPromise(new RejectedPromise($e)); } $waitLoop = $this->waitLoop; diff --git a/src/Symfony/Component/HttpClient/Response/HttplugPromise.php b/src/Symfony/Component/HttpClient/Response/HttplugPromise.php index 2f98d6e0b92ec..f3806c9e53e2e 100644 --- a/src/Symfony/Component/HttpClient/Response/HttplugPromise.php +++ b/src/Symfony/Component/HttpClient/Response/HttplugPromise.php @@ -54,6 +54,12 @@ public function getState(): string */ public function wait($unwrap = true) { - return $this->promise->wait($unwrap); + $result = $this->promise->wait($unwrap); + + while ($result instanceof HttplugPromiseInterface || $result instanceof GuzzlePromiseInterface) { + $result = $result->wait($unwrap); + } + + return $result; } } diff --git a/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php b/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php index 6a8cadbbc88a8..1f48be5c574c2 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php @@ -11,13 +11,18 @@ namespace Symfony\Component\HttpClient\Tests; +use GuzzleHttp\Promise\FulfilledPromise as GuzzleFulfilledPromise; use Http\Client\Exception\NetworkException; use Http\Client\Exception\RequestException; +use Http\Promise\FulfilledPromise; use Http\Promise\Promise; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; +use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Component\HttpClient\HttplugClient; +use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\NativeHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Contracts\HttpClient\Test\TestHttpServer; class HttplugClientTest extends TestCase @@ -152,4 +157,114 @@ public function testRequestException() $this->expectException(RequestException::class); $client->sendRequest($client->createRequest('BAD.METHOD', 'http://localhost:8057')); } + + public function testRetry404() + { + $client = new HttplugClient(new NativeHttpClient()); + + $successCallableCalled = false; + $failureCallableCalled = false; + + $promise = $client + ->sendAsyncRequest($client->createRequest('GET', 'http://localhost:8057/404')) + ->then( + function (ResponseInterface $response) use (&$successCallableCalled, $client) { + $this->assertSame(404, $response->getStatusCode()); + $successCallableCalled = true; + + return $client->sendAsyncRequest($client->createRequest('GET', 'http://localhost:8057')); + }, + function (\Exception $exception) use (&$failureCallableCalled) { + $failureCallableCalled = true; + + throw $exception; + } + ) + ; + + $response = $promise->wait(true); + + $this->assertTrue($successCallableCalled); + $this->assertFalse($failureCallableCalled); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testRetryNetworkError() + { + $client = new HttplugClient(new NativeHttpClient()); + + $successCallableCalled = false; + $failureCallableCalled = false; + + $promise = $client + ->sendAsyncRequest($client->createRequest('GET', 'http://localhost:8057/chunked-broken')) + ->then(function (ResponseInterface $response) use (&$successCallableCalled) { + $successCallableCalled = true; + + return $response; + }, function (\Exception $exception) use (&$failureCallableCalled, $client) { + $this->assertSame(NetworkException::class, \get_class($exception)); + $this->assertSame(TransportException::class, \get_class($exception->getPrevious())); + $failureCallableCalled = true; + + return $client->sendAsyncRequest($client->createRequest('GET', 'http://localhost:8057')); + }) + ; + + $response = $promise->wait(true); + + $this->assertFalse($successCallableCalled); + $this->assertTrue($failureCallableCalled); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testRetryEarlierError() + { + $isFirstRequest = true; + $errorMessage = 'Error occurred before making the actual request.'; + + $client = new HttplugClient(new MockHttpClient(function () use (&$isFirstRequest, $errorMessage) { + if ($isFirstRequest) { + $isFirstRequest = false; + throw new TransportException($errorMessage); + } + + return new MockResponse('OK', ['http_code' => 200]); + })); + + $request = $client->createRequest('GET', 'http://test'); + + $successCallableCalled = false; + $failureCallableCalled = false; + + $promise = $client + ->sendAsyncRequest($request) + ->then( + function (ResponseInterface $response) use (&$successCallableCalled) { + $successCallableCalled = true; + + return $response; + }, + function (\Exception $exception) use ($errorMessage, &$failureCallableCalled, $client, $request) { + $this->assertSame(NetworkException::class, \get_class($exception)); + $this->assertSame($errorMessage, $exception->getMessage()); + $failureCallableCalled = true; + + // Ensure arbitrary levels of promises work. + return (new FulfilledPromise(null))->then(function () use ($client, $request) { + return (new GuzzleFulfilledPromise(null))->then(function () use ($client, $request) { + return $client->sendAsyncRequest($request); + }); + }); + } + ) + ; + + $response = $promise->wait(true); + + $this->assertFalse($successCallableCalled); + $this->assertTrue($failureCallableCalled); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('OK', (string) $response->getBody()); + } }
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: