From 01228fa89454b00695f68c84e177b400715ff081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 23 Nov 2022 14:06:26 +0100 Subject: [PATCH 1/2] Rename internal `Request` to `ClientRequestStream` --- src/Client/Client.php | 6 ++- .../ClientRequestStream.php} | 7 +-- .../ClientRequestStreamTest.php} | 48 +++++++++---------- tests/Io/SenderTest.php | 26 +++++----- 4 files changed, 45 insertions(+), 42 deletions(-) rename src/{Client/Request.php => Io/ClientRequestStream.php} (96%) rename tests/{Client/RequestTest.php => Io/ClientRequestStreamTest.php} (89%) diff --git a/src/Client/Client.php b/src/Client/Client.php index 7a97349c..62caed5f 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -3,8 +3,9 @@ namespace React\Http\Client; use React\EventLoop\LoopInterface; -use React\Socket\ConnectorInterface; +use React\Http\Io\ClientRequestStream; use React\Socket\Connector; +use React\Socket\ConnectorInterface; /** * @internal @@ -22,10 +23,11 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector = $this->connector = $connector; } + /** @return ClientRequestStream */ public function request($method, $url, array $headers = array(), $protocolVersion = '1.0') { $requestData = new RequestData($method, $url, $headers, $protocolVersion); - return new Request($this->connector, $requestData); + return new ClientRequestStream($this->connector, $requestData); } } diff --git a/src/Client/Request.php b/src/Io/ClientRequestStream.php similarity index 96% rename from src/Client/Request.php rename to src/Io/ClientRequestStream.php index 51e03313..2513a89a 100644 --- a/src/Client/Request.php +++ b/src/Io/ClientRequestStream.php @@ -1,8 +1,9 @@ write($headers . $pendingWrites); - $stateRef = Request::STATE_HEAD_WRITTEN; + $stateRef = ClientRequestStream::STATE_HEAD_WRITTEN; // clear pending writes if non-empty if ($pendingWrites !== '') { diff --git a/tests/Client/RequestTest.php b/tests/Io/ClientRequestStreamTest.php similarity index 89% rename from tests/Client/RequestTest.php rename to tests/Io/ClientRequestStreamTest.php index cdb209cf..6e3e16b8 100644 --- a/tests/Client/RequestTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -1,16 +1,16 @@ connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -67,7 +67,7 @@ public function requestShouldBindToStreamEventsAndUseconnector() public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme() { $requestData = new RequestData('GET', 'https://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once())->method('connect')->with('tls://www.example.com:443')->willReturn(new Promise(function () { })); @@ -78,7 +78,7 @@ public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme() public function requestShouldEmitErrorIfConnectionFails() { $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException())); @@ -94,7 +94,7 @@ public function requestShouldEmitErrorIfConnectionFails() public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() { $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -111,7 +111,7 @@ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() public function requestShouldEmitErrorIfConnectionEmitsError() { $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -128,7 +128,7 @@ public function requestShouldEmitErrorIfConnectionEmitsError() public function requestShouldEmitErrorIfRequestParserThrowsException() { $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -144,7 +144,7 @@ public function requestShouldEmitErrorIfRequestParserThrowsException() public function requestShouldEmitErrorIfUrlIsInvalid() { $requestData = new RequestData('GET', 'ftp://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); @@ -160,7 +160,7 @@ public function requestShouldEmitErrorIfUrlIsInvalid() public function requestShouldEmitErrorIfUrlHasNoScheme() { $requestData = new RequestData('GET', 'www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); @@ -174,7 +174,7 @@ public function requestShouldEmitErrorIfUrlHasNoScheme() public function postRequestShouldSendAPostRequest() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -194,7 +194,7 @@ public function postRequestShouldSendAPostRequest() public function writeWithAPostRequestShouldSendToTheStream() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -217,7 +217,7 @@ public function writeWithAPostRequestShouldSendToTheStream() public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $resolveConnection = $this->successfulAsyncConnectionMock(); @@ -248,7 +248,7 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsBuffer() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->stream = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() @@ -285,7 +285,7 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB public function pipeShouldPipeDataIntoTheRequestBody() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -318,7 +318,7 @@ public function pipeShouldPipeDataIntoTheRequestBody() public function writeShouldStartConnecting() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once()) ->method('connect') @@ -334,7 +334,7 @@ public function writeShouldStartConnecting() public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once()) ->method('connect') @@ -352,7 +352,7 @@ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() public function closeShouldEmitCloseEvent() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $request->on('close', $this->expectCallableOnce()); $request->close(); @@ -364,7 +364,7 @@ public function closeShouldEmitCloseEvent() public function writeAfterCloseReturnsFalse() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $request->close(); @@ -378,7 +378,7 @@ public function writeAfterCloseReturnsFalse() public function endAfterCloseIsNoOp() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->never()) ->method('connect'); @@ -393,7 +393,7 @@ public function endAfterCloseIsNoOp() public function closeShouldCancelPendingConnectionAttempt() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $promise = new Promise(function () {}, function () { throw new \RuntimeException(); @@ -417,7 +417,7 @@ public function closeShouldCancelPendingConnectionAttempt() public function requestShouldRemoveAllListenerAfterClosed() { $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $request->on('close', function () {}); $this->assertCount(1, $request->listeners('close')); @@ -451,7 +451,7 @@ private function successfulAsyncConnectionMock() public function multivalueHeader() { $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 91b87b30..6d8c3b5f 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -76,7 +76,7 @@ public function testSendPostWillAutomaticallySendContentLengthHeader() 'http://www.google.com/', array('Host' => 'www.google.com', 'Content-Length' => '5'), '1.1' - )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -92,7 +92,7 @@ public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmpty 'http://www.google.com/', array('Host' => 'www.google.com', 'Content-Length' => '0'), '1.1' - )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -102,7 +102,7 @@ public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmpty public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() { - $outgoing = $this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('write')->with(""); $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); @@ -122,7 +122,7 @@ public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAndRespectRequestThrottling() { - $outgoing = $this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->exactly(2))->method('write')->withConsecutive(array(""), array("5\r\nhello\r\n"))->willReturn(false); @@ -141,7 +141,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAn public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() { - $outgoing = $this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->exactly(2))->method('write')->withConsecutive(array(""), array("0\r\n\r\n"))->willReturn(false); $outgoing->expects($this->once())->method('end')->with(null); @@ -160,7 +160,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() { - $outgoing = $this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->once())->method('write')->with("")->willReturn(false); $outgoing->expects($this->never())->method('end'); @@ -190,7 +190,7 @@ public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() { - $outgoing = $this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->once())->method('write')->with("")->willReturn(false); $outgoing->expects($this->never())->method('end'); @@ -218,7 +218,7 @@ public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() { - $outgoing = $this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->exactly(2))->method('write')->withConsecutive(array(""), array("0\r\n\r\n"))->willReturn(false); $outgoing->expects($this->once())->method('end'); @@ -252,7 +252,7 @@ public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() 'http://www.google.com/', array('Host' => 'www.google.com', 'Content-Length' => '100'), '1.1' - )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -269,7 +269,7 @@ public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() 'http://www.google.com/', array('Host' => 'www.google.com'), '1.1' - )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -285,7 +285,7 @@ public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyReques 'http://www.google.com/', array('Host' => 'www.google.com'), '1.1' - )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -301,7 +301,7 @@ public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsI 'http://www.google.com/', array('Host' => 'www.google.com', 'Content-Length' => '0'), '1.1' - )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -393,7 +393,7 @@ public function testRequestProtocolVersion(Request $Request, $method, $uri, $hea $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(), ))->getMock(); - $request = $this->getMockBuilder('React\Http\Client\Request') + $request = $this->getMockBuilder('React\Http\Io\ClientRequestStream') ->setMethods(array()) ->setConstructorArgs(array( $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(), From 212a3bba511f307eb4efb311e5afafb68b3ecd05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 24 Nov 2022 16:45:11 +0100 Subject: [PATCH 2/2] Refactor to remove internal `RequestData` --- src/Client/Client.php | 7 +- src/Client/RequestData.php | 127 -------------- src/Io/ClientRequestStream.php | 39 +++-- src/Io/Sender.php | 16 +- tests/Client/FunctionalIntegrationTest.php | 13 +- tests/Client/RequestDataTest.php | 146 ---------------- tests/Io/ClientRequestStreamTest.php | 87 +++++++--- tests/Io/SenderTest.php | 185 ++++++++++----------- 8 files changed, 205 insertions(+), 415 deletions(-) delete mode 100644 src/Client/RequestData.php delete mode 100644 tests/Client/RequestDataTest.php diff --git a/src/Client/Client.php b/src/Client/Client.php index 62caed5f..c3fd4570 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -2,6 +2,7 @@ namespace React\Http\Client; +use Psr\Http\Message\RequestInterface; use React\EventLoop\LoopInterface; use React\Http\Io\ClientRequestStream; use React\Socket\Connector; @@ -24,10 +25,8 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector = } /** @return ClientRequestStream */ - public function request($method, $url, array $headers = array(), $protocolVersion = '1.0') + public function request(RequestInterface $request) { - $requestData = new RequestData($method, $url, $headers, $protocolVersion); - - return new ClientRequestStream($this->connector, $requestData); + return new ClientRequestStream($this->connector, $request); } } diff --git a/src/Client/RequestData.php b/src/Client/RequestData.php deleted file mode 100644 index 04bb4cad..00000000 --- a/src/Client/RequestData.php +++ /dev/null @@ -1,127 +0,0 @@ -method = $method; - $this->url = $url; - $this->headers = $headers; - $this->protocolVersion = $protocolVersion; - } - - private function mergeDefaultheaders(array $headers) - { - $port = ($this->getDefaultPort() === $this->getPort()) ? '' : ":{$this->getPort()}"; - $connectionHeaders = ('1.1' === $this->protocolVersion) ? array('Connection' => 'close') : array(); - $authHeaders = $this->getAuthHeaders(); - - $defaults = array_merge( - array( - 'Host' => $this->getHost().$port, - ), - $connectionHeaders, - $authHeaders - ); - - // remove all defaults that already exist in $headers - $lower = array_change_key_case($headers, CASE_LOWER); - foreach ($defaults as $key => $_) { - if (isset($lower[strtolower($key)])) { - unset($defaults[$key]); - } - } - - return array_merge($defaults, $headers); - } - - public function getScheme() - { - return parse_url($this->url, PHP_URL_SCHEME); - } - - public function getHost() - { - return parse_url($this->url, PHP_URL_HOST); - } - - public function getPort() - { - return (int) parse_url($this->url, PHP_URL_PORT) ?: $this->getDefaultPort(); - } - - public function getDefaultPort() - { - return ('https' === $this->getScheme()) ? 443 : 80; - } - - public function getPath() - { - $path = parse_url($this->url, PHP_URL_PATH); - $queryString = parse_url($this->url, PHP_URL_QUERY); - - // assume "/" path by default, but allow "OPTIONS *" - if ($path === null) { - $path = ($this->method === 'OPTIONS' && $queryString === null) ? '*': '/'; - } - if ($queryString !== null) { - $path .= '?' . $queryString; - } - - return $path; - } - - public function setProtocolVersion($version) - { - $this->protocolVersion = $version; - } - - public function __toString() - { - $headers = $this->mergeDefaultheaders($this->headers); - - $data = ''; - $data .= "{$this->method} {$this->getPath()} HTTP/{$this->protocolVersion}\r\n"; - foreach ($headers as $name => $values) { - foreach ((array)$values as $value) { - $data .= "$name: $value\r\n"; - } - } - $data .= "\r\n"; - - return $data; - } - - private function getUrlUserPass() - { - $components = parse_url($this->url); - - if (isset($components['user'])) { - return array( - 'user' => $components['user'], - 'pass' => isset($components['pass']) ? $components['pass'] : null, - ); - } - } - - private function getAuthHeaders() - { - if (null !== $auth = $this->getUrlUserPass()) { - return array( - 'Authorization' => 'Basic ' . base64_encode($auth['user'].':'.$auth['pass']), - ); - } - - return array(); - } -} diff --git a/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index 2513a89a..29536e88 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -3,7 +3,7 @@ namespace React\Http\Io; use Evenement\EventEmitter; -use React\Http\Client\RequestData; +use Psr\Http\Message\RequestInterface; use React\Promise; use React\Socket\ConnectionInterface; use React\Socket\ConnectorInterface; @@ -24,10 +24,15 @@ class ClientRequestStream extends EventEmitter implements WritableStreamInterfac const STATE_HEAD_WRITTEN = 2; const STATE_END = 3; + /** @var ConnectorInterface */ private $connector; - private $requestData; + /** @var RequestInterface */ + private $request; + + /** @var ?ConnectionInterface */ private $stream; + private $buffer; private $responseFactory; private $state = self::STATE_INIT; @@ -35,10 +40,10 @@ class ClientRequestStream extends EventEmitter implements WritableStreamInterfac private $pendingWrites = ''; - public function __construct(ConnectorInterface $connector, RequestData $requestData) + public function __construct(ConnectorInterface $connector, RequestInterface $request) { $this->connector = $connector; - $this->requestData = $requestData; + $this->request = $request; } public function isWritable() @@ -50,7 +55,7 @@ private function writeHead() { $this->state = self::STATE_WRITING_HEAD; - $requestData = $this->requestData; + $request = $this->request; $streamRef = &$this->stream; $stateRef = &$this->state; $pendingWrites = &$this->pendingWrites; @@ -58,8 +63,9 @@ private function writeHead() $promise = $this->connect(); $promise->then( - function (ConnectionInterface $stream) use ($requestData, &$streamRef, &$stateRef, &$pendingWrites, $that) { + function (ConnectionInterface $stream) use ($request, &$streamRef, &$stateRef, &$pendingWrites, $that) { $streamRef = $stream; + assert($streamRef instanceof ConnectionInterface); $stream->on('drain', array($that, 'handleDrain')); $stream->on('data', array($that, 'handleData')); @@ -67,10 +73,17 @@ function (ConnectionInterface $stream) use ($requestData, &$streamRef, &$stateRe $stream->on('error', array($that, 'handleError')); $stream->on('close', array($that, 'handleClose')); - $headers = (string) $requestData; + assert($request instanceof RequestInterface); + $headers = "{$request->getMethod()} {$request->getRequestTarget()} HTTP/{$request->getProtocolVersion()}\r\n"; + foreach ($request->getHeaders() as $name => $values) { + foreach ($values as $value) { + $headers .= "$name: $value\r\n"; + } + } - $more = $stream->write($headers . $pendingWrites); + $more = $stream->write($headers . "\r\n" . $pendingWrites); + assert($stateRef === ClientRequestStream::STATE_WRITING_HEAD); $stateRef = ClientRequestStream::STATE_HEAD_WRITTEN; // clear pending writes if non-empty @@ -218,20 +231,24 @@ public function close() protected function connect() { - $scheme = $this->requestData->getScheme(); + $scheme = $this->request->getUri()->getScheme(); if ($scheme !== 'https' && $scheme !== 'http') { return Promise\reject( new \InvalidArgumentException('Invalid request URL given') ); } - $host = $this->requestData->getHost(); - $port = $this->requestData->getPort(); + $host = $this->request->getUri()->getHost(); + $port = $this->request->getUri()->getPort(); if ($scheme === 'https') { $host = 'tls://' . $host; } + if ($port === null) { + $port = $scheme === 'https' ? 443 : 80; + } + return $this->connector ->connect($host . ':' . $port); } diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 2f04c797..2e821f5a 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -74,6 +74,9 @@ public function __construct(HttpClient $http) */ public function send(RequestInterface $request) { + // support HTTP/1.1 and HTTP/1.0 only, ensured by `Browser` already + assert(\in_array($request->getProtocolVersion(), array('1.0', '1.1'), true)); + $body = $request->getBody(); $size = $body->getSize(); @@ -91,12 +94,17 @@ public function send(RequestInterface $request) $size = 0; } - $headers = array(); - foreach ($request->getHeaders() as $name => $values) { - $headers[$name] = implode(', ', $values); + // automatically add `Connection: close` request header for HTTP/1.1 requests to avoid connection reuse + if ($request->getProtocolVersion() === '1.1' && !$request->hasHeader('Connection')) { + $request = $request->withHeader('Connection', 'close'); + } + + // automatically add `Authorization: Basic …` request header if URL includes `user:pass@host` + if ($request->getUri()->getUserInfo() !== '' && !$request->hasHeader('Authorization')) { + $request = $request->withHeader('Authorization', 'Basic ' . \base64_encode($request->getUri()->getUserInfo())); } - $requestStream = $this->http->request($request->getMethod(), (string)$request->getUri(), $headers, $request->getProtocolVersion()); + $requestStream = $this->http->request($request); $deferred = new Deferred(function ($_, $reject) use ($requestStream) { // close request stream if request is cancelled diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index d95bf828..90d8444b 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -5,6 +5,7 @@ use Psr\Http\Message\ResponseInterface; use React\EventLoop\Loop; use React\Http\Client\Client; +use React\Http\Message\Request; use React\Promise\Deferred; use React\Promise\Stream; use React\Socket\ConnectionInterface; @@ -45,7 +46,7 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() $port = parse_url($socket->getAddress(), PHP_URL_PORT); $client = new Client(Loop::get()); - $request = $client->request('GET', 'http://localhost:' . $port); + $request = $client->request(new Request('GET', 'http://localhost:' . $port, array(), '', '1.0')); $promise = Stream\first($request, 'close'); $request->end(); @@ -62,7 +63,7 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp }); $client = new Client(Loop::get()); - $request = $client->request('GET', str_replace('tcp:', 'http:', $socket->getAddress())); + $request = $client->request(new Request('GET', str_replace('tcp:', 'http:', $socket->getAddress()), array(), '', '1.0')); $once = $this->expectCallableOnceWith('body'); $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) { @@ -83,7 +84,7 @@ public function testSuccessfulResponseEmitsEnd() $client = new Client(Loop::get()); - $request = $client->request('GET', 'http://www.google.com/'); + $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); $once = $this->expectCallableOnce(); $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) { @@ -109,7 +110,7 @@ public function testPostDataReturnsData() $client = new Client(Loop::get()); $data = str_repeat('.', 33000); - $request = $client->request('POST', 'https://' . (mt_rand(0, 1) === 0 ? 'eu.' : '') . 'httpbin.org/post', array('Content-Length' => strlen($data))); + $request = $client->request(new Request('POST', 'https://' . (mt_rand(0, 1) === 0 ? 'eu.' : '') . 'httpbin.org/post', array('Content-Length' => strlen($data)), '', '1.0')); $deferred = new Deferred(); $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred) { @@ -141,7 +142,7 @@ public function testPostJsonReturnsData() $client = new Client(Loop::get()); $data = json_encode(array('numbers' => range(1, 50))); - $request = $client->request('POST', 'https://httpbin.org/post', array('Content-Length' => strlen($data), 'Content-Type' => 'application/json')); + $request = $client->request(new Request('POST', 'https://httpbin.org/post', array('Content-Length' => strlen($data), 'Content-Type' => 'application/json'), '', '1.0')); $deferred = new Deferred(); $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred) { @@ -170,7 +171,7 @@ public function testCancelPendingConnectionEmitsClose() $client = new Client(Loop::get()); - $request = $client->request('GET', 'http://www.google.com/'); + $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); $request->on('error', $this->expectCallableNever()); $request->on('close', $this->expectCallableOnce()); $request->end(); diff --git a/tests/Client/RequestDataTest.php b/tests/Client/RequestDataTest.php deleted file mode 100644 index f6713e85..00000000 --- a/tests/Client/RequestDataTest.php +++ /dev/null @@ -1,146 +0,0 @@ -assertSame($expected, $requestData->__toString()); - } - - /** @test */ - public function toStringReturnsHTTPRequestMessageWithEmptyQueryString() - { - $requestData = new RequestData('GET', 'http://www.example.com/path?hello=world'); - - $expected = "GET /path?hello=world HTTP/1.0\r\n" . - "Host: www.example.com\r\n" . - "\r\n"; - - $this->assertSame($expected, $requestData->__toString()); - } - - /** @test */ - public function toStringReturnsHTTPRequestMessageWithZeroQueryStringAndRootPath() - { - $requestData = new RequestData('GET', 'http://www.example.com?0'); - - $expected = "GET /?0 HTTP/1.0\r\n" . - "Host: www.example.com\r\n" . - "\r\n"; - - $this->assertSame($expected, $requestData->__toString()); - } - - /** @test */ - public function toStringReturnsHTTPRequestMessageWithOptionsAbsoluteRequestForm() - { - $requestData = new RequestData('OPTIONS', 'http://www.example.com/'); - - $expected = "OPTIONS / HTTP/1.0\r\n" . - "Host: www.example.com\r\n" . - "\r\n"; - - $this->assertSame($expected, $requestData->__toString()); - } - - /** @test */ - public function toStringReturnsHTTPRequestMessageWithOptionsAsteriskRequestForm() - { - $requestData = new RequestData('OPTIONS', 'http://www.example.com'); - - $expected = "OPTIONS * HTTP/1.0\r\n" . - "Host: www.example.com\r\n" . - "\r\n"; - - $this->assertSame($expected, $requestData->__toString()); - } - - /** @test */ - public function toStringReturnsHTTPRequestMessageWithProtocolVersion() - { - $requestData = new RequestData('GET', 'http://www.example.com'); - $requestData->setProtocolVersion('1.1'); - - $expected = "GET / HTTP/1.1\r\n" . - "Host: www.example.com\r\n" . - "Connection: close\r\n" . - "\r\n"; - - $this->assertSame($expected, $requestData->__toString()); - } - - /** @test */ - public function toStringReturnsHTTPRequestMessageWithHeaders() - { - $requestData = new RequestData('GET', 'http://www.example.com', array( - 'User-Agent' => array(), - 'Via' => array( - 'first', - 'second' - ) - )); - - $expected = "GET / HTTP/1.0\r\n" . - "Host: www.example.com\r\n" . - "Via: first\r\n" . - "Via: second\r\n" . - "\r\n"; - - $this->assertSame($expected, $requestData->__toString()); - } - - /** @test */ - public function toStringReturnsHTTPRequestMessageWithHeadersInCustomCase() - { - $requestData = new RequestData('GET', 'http://www.example.com', array( - 'user-agent' => 'Hello', - 'LAST' => 'World' - )); - - $expected = "GET / HTTP/1.0\r\n" . - "Host: www.example.com\r\n" . - "user-agent: Hello\r\n" . - "LAST: World\r\n" . - "\r\n"; - - $this->assertSame($expected, $requestData->__toString()); - } - - /** @test */ - public function toStringReturnsHTTPRequestMessageWithProtocolVersionThroughConstructor() - { - $requestData = new RequestData('GET', 'http://www.example.com', array(), '1.1'); - - $expected = "GET / HTTP/1.1\r\n" . - "Host: www.example.com\r\n" . - "Connection: close\r\n" . - "\r\n"; - - $this->assertSame($expected, $requestData->__toString()); - } - - /** @test */ - public function toStringUsesUserPassFromURL() - { - $requestData = new RequestData('GET', 'http://john:dummy@www.example.com'); - - $expected = "GET / HTTP/1.0\r\n" . - "Host: www.example.com\r\n" . - "Authorization: Basic am9objpkdW1teQ==\r\n" . - "\r\n"; - - $this->assertSame($expected, $requestData->__toString()); - } -} diff --git a/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index 6e3e16b8..07a4eb73 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -2,12 +2,11 @@ namespace React\Tests\Http\Io; -use React\Http\Client\RequestData; use React\Http\Io\ClientRequestStream; -use React\Stream\DuplexResourceStream; -use React\Promise\RejectedPromise; +use React\Http\Message\Request; use React\Promise\Deferred; use React\Promise\Promise; +use React\Stream\DuplexResourceStream; use React\Tests\Http\TestCase; class ClientRequestStreamTest extends TestCase @@ -31,7 +30,7 @@ public function setUpStream() /** @test */ public function requestShouldBindToStreamEventsAndUseconnector() { - $requestData = new RequestData('GET', 'http://www.example.com'); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -66,7 +65,7 @@ public function requestShouldBindToStreamEventsAndUseconnector() */ public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme() { - $requestData = new RequestData('GET', 'https://www.example.com'); + $requestData = new Request('GET', 'https://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once())->method('connect')->with('tls://www.example.com:443')->willReturn(new Promise(function () { })); @@ -77,7 +76,7 @@ public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme() /** @test */ public function requestShouldEmitErrorIfConnectionFails() { - $requestData = new RequestData('GET', 'http://www.example.com'); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException())); @@ -93,7 +92,7 @@ public function requestShouldEmitErrorIfConnectionFails() /** @test */ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() { - $requestData = new RequestData('GET', 'http://www.example.com'); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -110,7 +109,7 @@ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() /** @test */ public function requestShouldEmitErrorIfConnectionEmitsError() { - $requestData = new RequestData('GET', 'http://www.example.com'); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -127,7 +126,7 @@ public function requestShouldEmitErrorIfConnectionEmitsError() /** @test */ public function requestShouldEmitErrorIfRequestParserThrowsException() { - $requestData = new RequestData('GET', 'http://www.example.com'); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -143,7 +142,7 @@ public function requestShouldEmitErrorIfRequestParserThrowsException() */ public function requestShouldEmitErrorIfUrlIsInvalid() { - $requestData = new RequestData('GET', 'ftp://www.example.com'); + $requestData = new Request('GET', 'ftp://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); @@ -159,7 +158,7 @@ public function requestShouldEmitErrorIfUrlIsInvalid() */ public function requestShouldEmitErrorIfUrlHasNoScheme() { - $requestData = new RequestData('GET', 'www.example.com'); + $requestData = new Request('GET', 'www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); @@ -170,10 +169,50 @@ public function requestShouldEmitErrorIfUrlHasNoScheme() $request->end(); } + /** @test */ + public function getRequestShouldSendAGetRequest() + { + $requestData = new Request('GET', 'http://www.example.com', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $this->stream->expects($this->once())->method('write')->with("GET / HTTP/1.0\r\nHost: www.example.com\r\n\r\n"); + + $request->end(); + } + + /** @test */ + public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionCloseHeader() + { + $requestData = new Request('GET', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $this->stream->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + + $request->end(); + } + + /** @test */ + public function getOptionsAsteriskShouldSendAOptionsRequestAsteriskRequestTarget() + { + $requestData = new Request('OPTIONS', 'http://www.example.com', array('Connection' => 'close'), '', '1.1'); + $requestData = $requestData->withRequestTarget('*'); + $request = new ClientRequestStream($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $this->stream->expects($this->once())->method('write')->with("OPTIONS * HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + + $request->end(); + } + /** @test */ public function postRequestShouldSendAPostRequest() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -193,7 +232,7 @@ public function postRequestShouldSendAPostRequest() /** @test */ public function writeWithAPostRequestShouldSendToTheStream() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -216,7 +255,7 @@ public function writeWithAPostRequestShouldSendToTheStream() /** @test */ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); $request = new ClientRequestStream($this->connector, $requestData); $resolveConnection = $this->successfulAsyncConnectionMock(); @@ -247,7 +286,7 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent /** @test */ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsBuffer() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); $request = new ClientRequestStream($this->connector, $requestData); $this->stream = $this->getMockBuilder('React\Socket\Connection') @@ -284,7 +323,7 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB /** @test */ public function pipeShouldPipeDataIntoTheRequestBody() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -317,7 +356,7 @@ public function pipeShouldPipeDataIntoTheRequestBody() */ public function writeShouldStartConnecting() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once()) @@ -333,7 +372,7 @@ public function writeShouldStartConnecting() */ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once()) @@ -351,7 +390,7 @@ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() */ public function closeShouldEmitCloseEvent() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -363,7 +402,7 @@ public function closeShouldEmitCloseEvent() */ public function writeAfterCloseReturnsFalse() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $request->close(); @@ -377,7 +416,7 @@ public function writeAfterCloseReturnsFalse() */ public function endAfterCloseIsNoOp() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->never()) @@ -392,7 +431,7 @@ public function endAfterCloseIsNoOp() */ public function closeShouldCancelPendingConnectionAttempt() { - $requestData = new RequestData('POST', 'http://www.example.com'); + $requestData = new Request('POST', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $promise = new Promise(function () {}, function () { @@ -416,7 +455,7 @@ public function closeShouldCancelPendingConnectionAttempt() /** @test */ public function requestShouldRemoveAllListenerAfterClosed() { - $requestData = new RequestData('GET', 'http://www.example.com'); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $request->on('close', function () {}); @@ -450,7 +489,7 @@ private function successfulAsyncConnectionMock() /** @test */ public function multivalueHeader() { - $requestData = new RequestData('GET', 'http://www.example.com'); + $requestData = new Request('GET', 'http://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 6d8c3b5f..c2357a1a 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -2,8 +2,8 @@ namespace React\Tests\Http\Io; +use Psr\Http\Message\RequestInterface; use React\Http\Client\Client as HttpClient; -use React\Http\Client\RequestData; use React\Http\Io\ReadableBodyStream; use React\Http\Io\Sender; use React\Http\Message\Request; @@ -71,12 +71,9 @@ public function testSenderConnectorRejection() public function testSendPostWillAutomaticallySendContentLengthHeader() { $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with( - 'POST', - 'http://www.google.com/', - array('Host' => 'www.google.com', 'Content-Length' => '5'), - '1.1' - )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Content-Length') === '5'; + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -87,12 +84,9 @@ public function testSendPostWillAutomaticallySendContentLengthHeader() public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmptyRequestBody() { $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with( - 'POST', - 'http://www.google.com/', - array('Host' => 'www.google.com', 'Content-Length' => '0'), - '1.1' - )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Content-Length') === '0'; + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -106,12 +100,9 @@ public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() $outgoing->expects($this->once())->method('write')->with(""); $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with( - 'POST', - 'http://www.google.com/', - array('Host' => 'www.google.com', 'Transfer-Encoding' => 'chunked'), - '1.1' - )->willReturn($outgoing); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Transfer-Encoding') === 'chunked'; + }))->willReturn($outgoing); $sender = new Sender($client); @@ -247,12 +238,9 @@ public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() { $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with( - 'POST', - 'http://www.google.com/', - array('Host' => 'www.google.com', 'Content-Length' => '100'), - '1.1' - )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Content-Length') === '100'; + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -264,12 +252,9 @@ public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() { $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with( - 'GET', - 'http://www.google.com/', - array('Host' => 'www.google.com'), - '1.1' - )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return !$request->hasHeader('Content-Length'); + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -280,12 +265,9 @@ public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyRequestBody() { $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with( - 'CUSTOM', - 'http://www.google.com/', - array('Host' => 'www.google.com'), - '1.1' - )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return !$request->hasHeader('Content-Length'); + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -296,12 +278,9 @@ public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyReques public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsIs() { $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with( - 'CUSTOM', - 'http://www.google.com/', - array('Host' => 'www.google.com', 'Content-Length' => '0'), - '1.1' - )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Content-Length') === '0'; + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -309,6 +288,76 @@ public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsI $sender->send($request); } + /** @test */ + public function getHttp10RequestShouldSendAGetRequestWithoutConnectionHeaderByDefault() + { + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return !$request->hasHeader('Connection'); + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client); + + $request = new Request('GET', 'http://www.example.com', array(), '', '1.0'); + $sender->send($request); + } + + /** @test */ + public function getHttp11RequestShouldSendAGetRequestWithConnectionCloseHeaderByDefault() + { + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Connection') === 'close'; + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client); + + $request = new Request('GET', 'http://www.example.com', array(), '', '1.1'); + $sender->send($request); + } + + /** @test */ + public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionUpgradeHeader() + { + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Connection') === 'upgrade'; + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client); + + $request = new Request('GET', 'http://www.example.com', array('Connection' => 'upgrade'), '', '1.1'); + $sender->send($request); + } + + /** @test */ + public function getRequestWithUserAndPassShouldSendAGetRequestWithBasicAuthorizationHeader() + { + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Authorization') === 'Basic am9objpkdW1teQ=='; + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client); + + $request = new Request('GET', 'http://john:dummy@www.example.com'); + $sender->send($request); + } + + /** @test */ + public function getRequestWithUserAndPassShouldSendAGetRequestWithGivenAuthorizationHeaderBasicAuthorizationHeader() + { + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Authorization') === 'bearer abc123'; + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client); + + $request = new Request('GET', 'http://john:dummy@www.example.com', array('Authorization' => 'bearer abc123')); + $sender->send($request); + } + public function testCancelRequestWillCancelConnector() { $promise = new \React\Promise\Promise(function () { }, function () { @@ -355,54 +404,4 @@ public function testCancelRequestWillCloseConnection() $this->assertInstanceOf('RuntimeException', $exception); } - - public function provideRequestProtocolVersion() - { - return array( - array( - new Request('GET', 'http://www.google.com/'), - 'GET', - 'http://www.google.com/', - array( - 'Host' => 'www.google.com', - ), - '1.1', - ), - array( - new Request('GET', 'http://www.google.com/', array(), '', '1.0'), - 'GET', - 'http://www.google.com/', - array( - 'Host' => 'www.google.com', - ), - '1.0', - ), - ); - } - - /** - * @dataProvider provideRequestProtocolVersion - */ - public function testRequestProtocolVersion(Request $Request, $method, $uri, $headers, $protocolVersion) - { - $http = $this->getMockBuilder('React\Http\Client\Client') - ->setMethods(array( - 'request', - )) - ->setConstructorArgs(array( - $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(), - ))->getMock(); - - $request = $this->getMockBuilder('React\Http\Io\ClientRequestStream') - ->setMethods(array()) - ->setConstructorArgs(array( - $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(), - new RequestData($method, $uri, $headers, $protocolVersion), - ))->getMock(); - - $http->expects($this->once())->method('request')->with($method, $uri, $headers, $protocolVersion)->willReturn($request); - - $sender = new Sender($http); - $sender->send($Request); - } }