diff --git a/composer.json b/composer.json index 071fd9e..7e22efe 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "react/http-client": "^0.5.6", "react/promise": "^2.2.1", "react/promise-stream": "^1.0 || ^0.1.1", + "react/promise-timer": "^1.2", "react/socket": "^1.0 || ^0.8.4", "react/stream": "^1.0 || ^0.7", "ringcentral/psr7": "^1.2" diff --git a/src/Browser.php b/src/Browser.php index ee3ba14..a952296 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -17,6 +17,9 @@ class Browser private $baseUri = null; private $options = array(); + /** @var LoopInterface $loop */ + private $loop; + /** * Instantiate the Browser * @@ -28,6 +31,7 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector = { $this->sender = Sender::createFromLoop($loop, $connector); $this->messageFactory = new MessageFactory(); + $this->loop = $loop; } public function get($url, $headers = array()) @@ -75,7 +79,7 @@ public function send(RequestInterface $request) $request = $request->withUri($this->messageFactory->expandBase($request->getUri(), $this->baseUri)); } - $transaction = new Transaction($request, $this->sender, $this->options, $this->messageFactory); + $transaction = new Transaction($request, $this->sender, $this->options, $this->messageFactory, $this->loop); return $transaction->send(); } diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index 4ec3b1e..9d267e0 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -8,6 +8,7 @@ use Clue\React\Buzz\Message\MessageFactory; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use React\EventLoop\LoopInterface; use React\Promise; use React\Promise\Stream; use React\Stream\ReadableStreamInterface; @@ -35,22 +36,43 @@ class Transaction private $streaming = false; - public function __construct(RequestInterface $request, Sender $sender, array $options = array(), MessageFactory $messageFactory) - { + /** @var int $timeout */ + private $timeout; + + /** @var LoopInterface $loop */ + private $loop; + + public function __construct( + RequestInterface $request, + Sender $sender, + array $options = array(), + MessageFactory $messageFactory, + LoopInterface $loop + ) { foreach ($options as $name => $value) { if (property_exists($this, $name)) { $this->$name = $value; } } + // In case the timeout hasn't been set through the options + if (empty($this->timeout) === true) { + $this->timeout = ini_get('default_socket_timeout'); + } + $this->request = $request; $this->sender = $sender; $this->messageFactory = $messageFactory; + $this->loop = $loop; } public function send() { - return $this->next($this->request); + return Promise\Timer\timeout( + $this->next($this->request), + $this->timeout, + $this->loop + ); } protected function next(RequestInterface $request) diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 0566255..229baa7 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -15,16 +15,17 @@ public function testReceivingErrorResponseWillRejectWithResponseException() { $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); $response = new Response(404); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); // mock sender to resolve promise with the given $response in response to the given $request $sender = $this->getMockBuilder('Clue\React\Buzz\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); - $transaction = new Transaction($request, $sender, array(), new MessageFactory()); + $transaction = new Transaction($request, $sender, array(), new MessageFactory(), $loop); $promise = $transaction->send(); try { - Block\await($promise, $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock()); + Block\await($promise, $loop); $this->fail(); } catch (ResponseException $exception) { $this->assertEquals(404, $exception->getCode()); @@ -50,7 +51,7 @@ public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefau $sender = $this->getMockBuilder('Clue\React\Buzz\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); - $transaction = new Transaction($request, $sender, array(), $messageFactory); + $transaction = new Transaction($request, $sender, array(), $messageFactory, $loop); $promise = $transaction->send(); $response = Block\await($promise, $loop); @@ -78,7 +79,7 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() $sender = $this->getMockBuilder('Clue\React\Buzz\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); - $transaction = new Transaction($request, $sender, array(), $messageFactory); + $transaction = new Transaction($request, $sender, array(), $messageFactory, $loop); $promise = $transaction->send(); $promise->cancel(); @@ -88,6 +89,7 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStreamingIsEnabled() { $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); $response = $messageFactory->response(1.0, 200, 'OK', array(), $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock()); @@ -96,12 +98,42 @@ public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStre $sender = $this->getMockBuilder('Clue\React\Buzz\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); - $transaction = new Transaction($request, $sender, array('streaming' => true), $messageFactory); + $transaction = new Transaction($request, $sender, array('streaming' => true), $messageFactory, $loop); $promise = $transaction->send(); - $response = Block\await($promise, $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock()); + $response = Block\await($promise, $loop); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('', (string)$response->getBody()); } + + public function testTimeoutPromiseWillThrowTimeoutException() + { + $messageFactory = new MessageFactory(); + $loop = Factory::create(); + + $stream = new ThroughStream(); + $loop->addTimer(0.01, function () use ($stream) { + $stream->close(); + }); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $response = $messageFactory->response(1.0, 200, 'OK', array(), $stream); + + // mock sender to resolve promise with the given $response in response to the given $request + $sender = $this->getMockBuilder('Clue\React\Buzz\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); + + $transaction = new Transaction($request, $sender, array('timeout' => 0.001), $messageFactory, $loop); + $promise = $transaction->send(); + + $exception = false; + try { + $response = Block\await($promise, $loop); + } catch (Exception $exception) { + + } + + $this->assertTrue(is_a($exception, 'React\Promise\Timer\TimeoutException')); + } }