diff --git a/src/Client/Client.php b/src/Client/Client.php index 7a97349c..c3fd4570 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -2,9 +2,11 @@ namespace React\Http\Client; +use Psr\Http\Message\RequestInterface; use React\EventLoop\LoopInterface; -use React\Socket\ConnectorInterface; +use React\Http\Io\ClientRequestStream; use React\Socket\Connector; +use React\Socket\ConnectorInterface; /** * @internal @@ -22,10 +24,9 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector = $this->connector = $connector; } - public function request($method, $url, array $headers = array(), $protocolVersion = '1.0') + /** @return ClientRequestStream */ + public function request(RequestInterface $request) { - $requestData = new RequestData($method, $url, $headers, $protocolVersion); - - return new Request($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/Client/Request.php b/src/Io/ClientRequestStream.php similarity index 80% rename from src/Client/Request.php rename to src/Io/ClientRequestStream.php index 51e03313..29536e88 100644 --- a/src/Client/Request.php +++ b/src/Io/ClientRequestStream.php @@ -1,8 +1,9 @@ connector = $connector; - $this->requestData = $requestData; + $this->request = $request; } public function isWritable() @@ -49,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; @@ -57,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')); @@ -66,11 +73,18 @@ 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); - $stateRef = Request::STATE_HEAD_WRITTEN; + assert($stateRef === ClientRequestStream::STATE_WRITING_HEAD); + $stateRef = ClientRequestStream::STATE_HEAD_WRITTEN; // clear pending writes if non-empty if ($pendingWrites !== '') { @@ -217,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/Client/RequestTest.php b/tests/Io/ClientRequestStreamTest.php similarity index 72% rename from tests/Client/RequestTest.php rename to tests/Io/ClientRequestStreamTest.php index cdb209cf..07a4eb73 100644 --- a/tests/Client/RequestTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -1,16 +1,15 @@ connector, $requestData); + $requestData = new Request('GET', 'http://www.example.com'); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -66,8 +65,8 @@ public function requestShouldBindToStreamEventsAndUseconnector() */ public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme() { - $requestData = new RequestData('GET', 'https://www.example.com'); - $request = new Request($this->connector, $requestData); + $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,8 +76,8 @@ public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme() /** @test */ public function requestShouldEmitErrorIfConnectionFails() { - $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $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,8 +92,8 @@ public function requestShouldEmitErrorIfConnectionFails() /** @test */ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() { - $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestData = new Request('GET', 'http://www.example.com'); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -110,8 +109,8 @@ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() /** @test */ public function requestShouldEmitErrorIfConnectionEmitsError() { - $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestData = new Request('GET', 'http://www.example.com'); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -127,8 +126,8 @@ public function requestShouldEmitErrorIfConnectionEmitsError() /** @test */ public function requestShouldEmitErrorIfRequestParserThrowsException() { - $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestData = new Request('GET', 'http://www.example.com'); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -143,8 +142,8 @@ public function requestShouldEmitErrorIfRequestParserThrowsException() */ public function requestShouldEmitErrorIfUrlIsInvalid() { - $requestData = new RequestData('GET', 'ftp://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestData = new Request('GET', 'ftp://www.example.com'); + $request = new ClientRequestStream($this->connector, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); @@ -159,8 +158,8 @@ public function requestShouldEmitErrorIfUrlIsInvalid() */ public function requestShouldEmitErrorIfUrlHasNoScheme() { - $requestData = new RequestData('GET', 'www.example.com'); - $request = new Request($this->connector, $requestData); + $requestData = new Request('GET', 'www.example.com'); + $request = new ClientRequestStream($this->connector, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); @@ -170,11 +169,51 @@ 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'); - $request = new Request($this->connector, $requestData); + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -193,8 +232,8 @@ public function postRequestShouldSendAPostRequest() /** @test */ public function writeWithAPostRequestShouldSendToTheStream() { - $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -216,8 +255,8 @@ public function writeWithAPostRequestShouldSendToTheStream() /** @test */ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent() { - $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); $resolveConnection = $this->successfulAsyncConnectionMock(); @@ -247,8 +286,8 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent /** @test */ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsBuffer() { - $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); $this->stream = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() @@ -284,8 +323,8 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB /** @test */ public function pipeShouldPipeDataIntoTheRequestBody() { - $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestData = new Request('POST', 'http://www.example.com', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -317,8 +356,8 @@ public function pipeShouldPipeDataIntoTheRequestBody() */ public function writeShouldStartConnecting() { - $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestData = new Request('POST', 'http://www.example.com'); + $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once()) ->method('connect') @@ -333,8 +372,8 @@ public function writeShouldStartConnecting() */ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() { - $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestData = new Request('POST', 'http://www.example.com'); + $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once()) ->method('connect') @@ -351,8 +390,8 @@ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() */ public function closeShouldEmitCloseEvent() { - $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestData = new Request('POST', 'http://www.example.com'); + $request = new ClientRequestStream($this->connector, $requestData); $request->on('close', $this->expectCallableOnce()); $request->close(); @@ -363,8 +402,8 @@ public function closeShouldEmitCloseEvent() */ public function writeAfterCloseReturnsFalse() { - $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestData = new Request('POST', 'http://www.example.com'); + $request = new ClientRequestStream($this->connector, $requestData); $request->close(); @@ -377,8 +416,8 @@ public function writeAfterCloseReturnsFalse() */ public function endAfterCloseIsNoOp() { - $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestData = new Request('POST', 'http://www.example.com'); + $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->never()) ->method('connect'); @@ -392,8 +431,8 @@ public function endAfterCloseIsNoOp() */ public function closeShouldCancelPendingConnectionAttempt() { - $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestData = new Request('POST', 'http://www.example.com'); + $request = new ClientRequestStream($this->connector, $requestData); $promise = new Promise(function () {}, function () { throw new \RuntimeException(); @@ -416,8 +455,8 @@ public function closeShouldCancelPendingConnectionAttempt() /** @test */ public function requestShouldRemoveAllListenerAfterClosed() { - $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestData = new Request('GET', 'http://www.example.com'); + $request = new ClientRequestStream($this->connector, $requestData); $request->on('close', function () {}); $this->assertCount(1, $request->listeners('close')); @@ -450,8 +489,8 @@ private function successfulAsyncConnectionMock() /** @test */ public function multivalueHeader() { - $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $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 91b87b30..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\Client\Request')->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\Client\Request')->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); @@ -102,16 +96,13 @@ 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(); - $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); @@ -122,7 +113,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 +132,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 +151,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 +181,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 +209,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'); @@ -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\Client\Request')->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\Client\Request')->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\Client\Request')->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\Client\Request')->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\Client\Request') - ->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); - } }