diff --git a/src/Symfony/Component/HttpClient/HttplugClient.php b/src/Symfony/Component/HttpClient/HttplugClient.php index ec00b3234a..5d691e024d 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\RequestFactory; 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 @@ final class HttplugClient implements HttplugInterface, HttpAsyncClient, RequestF 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 2f98d6e0b9..f3806c9e53 100644 --- a/src/Symfony/Component/HttpClient/Response/HttplugPromise.php +++ b/src/Symfony/Component/HttpClient/Response/HttplugPromise.php @@ -54,6 +54,12 @@ final class HttplugPromise implements HttplugPromiseInterface */ 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 6a8cadbbc8..1f48be5c57 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 @@ class HttplugClientTest extends TestCase $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()); + } }