diff --git a/README.md b/README.md index 762ee16..a405cb4 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ mess with most of the low-level details. * [withOptions()](#withoptions) * [withBase()](#withbase) * [withoutBase()](#withoutbase) + * [withProtocolVersion()](#withprotocolversion) * [ResponseInterface](#responseinterface) * [RequestInterface](#requestinterface) * [UriInterface](#uriinterface) @@ -126,15 +127,26 @@ $browser->put($url, array $headers = array(), string|ReadableStreamInterface $co $browser->patch($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); ``` -Each method will automatically add a matching `Content-Length` request header if -the size of the outgoing request body is known and non-empty. For an empty -request body, if will only include a `Content-Length: 0` request header if the -request method usually expects a request body (only applies to `POST`, `PUT` and -`PATCH`). If you're using a [streaming request body](#streaming), it will -default to using `Transfer-Encoding: chunked` unless you explicitly pass in a -matching `Content-Length` request header. -All the above methods default to sending requests as HTTP/1.0. -If you need a custom HTTP protocol method or version, you can use the [`send()`](#send) method. +Each of these methods requires a `$url` and some optional parameters to send an +HTTP request. Each of these method names matches the respective HTTP request +method, for example the `get()` method sends an HTTP `GET` request. + +You can optionally pass an associative array of additional `$headers` that will be +sent with this HTTP request. Additionally, each method will automatically add a +matching `Content-Length` request header if an outgoing request body is given and its +size is known and non-empty. For an empty request body, if will only include a +`Content-Length: 0` request header if the request method usually expects a request +body (only applies to `POST`, `PUT` and `PATCH` HTTP request methods). + +If you're using a [streaming request body](#streaming), it will default to using +`Transfer-Encoding: chunked` unless you explicitly pass in a matching `Content-Length` request +header. See also [streaming](#streaming) for more details. + +By default, all of the above methods default to sending requests using the +HTTP/1.1 protocol version. If you want to explicitly use the legacy HTTP/1.0 +protocol version, you can use the [`withProtocolVersion()`](#withprotocolversion) +method. If you want to use any other or even custom HTTP request method, you can +use the [`send()`](#send) method. Each of the above methods supports async operation and either *resolves* with a [`ResponseInterface`](#responseinterface) or *rejects* with an `Exception`. @@ -517,13 +529,14 @@ $browser->submit($url, array('user' => 'test', 'password' => 'secret')); The `send(RequestInterface $request): PromiseInterface` method can be used to send an arbitrary instance implementing the [`RequestInterface`](#requestinterface) (PSR-7). -All the above [predefined methods](#methods) default to sending requests as HTTP/1.0. -If you need a custom HTTP protocol method or version, then you may want to use this -method: +The preferred way to send an HTTP request is by using the above request +methods, for example the `get()` method to send an HTTP `GET` request. + +As an alternative, if you want to use a custom HTTP request method, you +can use this method: ```php $request = new Request('OPTIONS', $url); -$request = $request->withProtocolVersion('1.1'); $browser->send($request)->then(…); ``` @@ -599,6 +612,29 @@ actually returns a *new* [`Browser`](#browser) instance without any base URI app See also [`withBase()`](#withbase). +#### withProtocolVersion() + +The `withProtocolVersion(string $protocolVersion): Browser` method can be used to +change the HTTP protocol version that will be used for all subsequent requests. + +All the above [request methods](#methods) default to sending requests as +HTTP/1.1. This is the preferred HTTP protocol version which also provides +decent backwards-compatibility with legacy HTTP/1.0 servers. As such, +there should rarely be a need to explicitly change this protocol version. + +If you want to explicitly use the legacy HTTP/1.0 protocol version, you +can use this method: + +```php +$newBrowser = $browser->withProtocolVersion('1.0'); + +$newBrowser->get($url)->then(…); +``` + +Notice that the [`Browser`](#browser) is an immutable object, i.e. this +method actually returns a *new* [`Browser`](#browser) instance with the +new protocol version applied. + ### ResponseInterface The `Psr\Http\Message\ResponseInterface` represents the incoming response received from the [`Browser`](#browser). diff --git a/src/Browser.php b/src/Browser.php index eccf177..6bb1a99 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -18,6 +18,7 @@ class Browser private $transaction; private $messageFactory; private $baseUri = null; + private $protocolVersion = '1.1'; /** @var LoopInterface $loop */ private $loop; @@ -73,7 +74,7 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector = */ public function get($url, array $headers = array()) { - return $this->send($this->messageFactory->request('GET', $url, $headers)); + return $this->requestMayBeStreaming('GET', $url, $headers); } /** @@ -100,7 +101,7 @@ public function get($url, array $headers = array()) */ public function post($url, array $headers = array(), $contents = '') { - return $this->send($this->messageFactory->request('POST', $url, $headers, $contents)); + return $this->requestMayBeStreaming('POST', $url, $headers, $contents); } /** @@ -110,7 +111,7 @@ public function post($url, array $headers = array(), $contents = '') */ public function head($url, array $headers = array()) { - return $this->send($this->messageFactory->request('HEAD', $url, $headers)); + return $this->requestMayBeStreaming('HEAD', $url, $headers); } /** @@ -137,7 +138,7 @@ public function head($url, array $headers = array()) */ public function patch($url, array $headers = array(), $contents = '') { - return $this->send($this->messageFactory->request('PATCH', $url , $headers, $contents)); + return $this->requestMayBeStreaming('PATCH', $url , $headers, $contents); } /** @@ -164,7 +165,7 @@ public function patch($url, array $headers = array(), $contents = '') */ public function put($url, array $headers = array(), $contents = '') { - return $this->send($this->messageFactory->request('PUT', $url, $headers, $contents)); + return $this->requestMayBeStreaming('PUT', $url, $headers, $contents); } /** @@ -175,7 +176,7 @@ public function put($url, array $headers = array(), $contents = '') */ public function delete($url, array $headers = array(), $contents = '') { - return $this->send($this->messageFactory->request('DELETE', $url, $headers, $contents)); + return $this->requestMayBeStreaming('DELETE', $url, $headers, $contents); } /** @@ -199,19 +200,20 @@ public function submit($url, array $fields, $headers = array(), $method = 'POST' $headers['Content-Type'] = 'application/x-www-form-urlencoded'; $contents = http_build_query($fields); - return $this->send($this->messageFactory->request($method, $url, $headers, $contents)); + return $this->requestMayBeStreaming($method, $url, $headers, $contents); } /** * Sends an arbitrary instance implementing the [`RequestInterface`](#requestinterface) (PSR-7). * - * All the above [predefined methods](#methods) default to sending requests as HTTP/1.0. - * If you need a custom HTTP protocol method or version, then you may want to use this - * method: + * The preferred way to send an HTTP request is by using the above request + * methods, for example the `get()` method to send an HTTP `GET` request. + * + * As an alternative, if you want to use a custom HTTP request method, you + * can use this method: * * ```php * $request = new Request('OPTIONS', $url); - * $request = $request->withProtocolVersion('1.1'); * * $browser->send($request)->then(…); * ``` @@ -335,4 +337,54 @@ public function withOptions(array $options) return $browser; } + + /** + * Changes the HTTP protocol version that will be used for all subsequent requests. + * + * All the above [request methods](#methods) default to sending requests as + * HTTP/1.1. This is the preferred HTTP protocol version which also provides + * decent backwards-compatibility with legacy HTTP/1.0 servers. As such, + * there should rarely be a need to explicitly change this protocol version. + * + * If you want to explicitly use the legacy HTTP/1.0 protocol version, you + * can use this method: + * + * ```php + * $newBrowser = $browser->withProtocolVersion('1.0'); + * + * $newBrowser->get($url)->then(…); + * ``` + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * new protocol version applied. + * + * @param string $protocolVersion HTTP protocol version to use, must be one of "1.1" or "1.0" + * @return self + * @throws InvalidArgumentException + * @since 2.8.0 + */ + public function withProtocolVersion($protocolVersion) + { + if (!\in_array($protocolVersion, array('1.0', '1.1'), true)) { + throw new InvalidArgumentException('Invalid HTTP protocol version, must be one of "1.1" or "1.0"'); + } + + $browser = clone $this; + $browser->protocolVersion = (string) $protocolVersion; + + return $browser; + } + + /** + * @param string $method + * @param string|UriInterface $url + * @param array $headers + * @param string|ReadableStreamInterface $contents + * @return PromiseInterface + */ + private function requestMayBeStreaming($method, $url, array $headers = array(), $contents = '') + { + return $this->send($this->messageFactory->request($method, $url, $headers, $contents, $this->protocolVersion)); + } } diff --git a/src/Message/MessageFactory.php b/src/Message/MessageFactory.php index ccc1413..ac7c08a 100644 --- a/src/Message/MessageFactory.php +++ b/src/Message/MessageFactory.php @@ -21,17 +21,18 @@ class MessageFactory * @param string|UriInterface $uri * @param array $headers * @param string|ReadableStreamInterface $content + * @param string $protocolVersion * @return Request */ - public function request($method, $uri, $headers = array(), $content = '') + public function request($method, $uri, $headers = array(), $content = '', $protocolVersion = '1.1') { - return new Request($method, $uri, $headers, $this->body($content), '1.0'); + return new Request($method, $uri, $headers, $this->body($content), $protocolVersion); } /** * Creates a new instance of ResponseInterface for the given response parameters * - * @param string $version + * @param string $protocolVersion * @param int $status * @param string $reason * @param array $headers @@ -39,9 +40,9 @@ public function request($method, $uri, $headers = array(), $content = '') * @return Response * @uses self::body() */ - public function response($version, $status, $reason, $headers = array(), $body = '') + public function response($protocolVersion, $status, $reason, $headers = array(), $body = '') { - $response = new Response($status, $headers, $body instanceof ReadableStreamInterface ? null : $body, $version, $reason); + $response = new Response($status, $headers, $body instanceof ReadableStreamInterface ? null : $body, $protocolVersion, $reason); if ($body instanceof ReadableStreamInterface) { $length = null; diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index 8edeb32..fd7f250 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -26,6 +26,85 @@ public function setUp() $ref->setValue($this->browser, $this->sender); } + public function testGetSendsGetRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('GET', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('http://example.com/'); + } + + public function testPostSendsPostRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('POST', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->post('http://example.com/'); + } + + public function testHeadSendsHeadRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('HEAD', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->head('http://example.com/'); + } + + public function testPatchSendsPatchRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('PATCH', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->patch('http://example.com/'); + } + + public function testPutSendsPutRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('PUT', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->put('http://example.com/'); + } + + public function testDeleteSendsDeleteRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('DELETE', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->delete('http://example.com/'); + } + + public function testSubmitSendsPostRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('POST', $request->getMethod()); + $that->assertEquals('application/x-www-form-urlencoded', $request->getHeaderLine('Content-Type')); + $that->assertEquals('', (string)$request->getBody()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->submit('http://example.com/', array()); + } + public function testWithBase() { $browser = $this->browser->withBase('http://example.com/root'); @@ -166,6 +245,40 @@ public function testWithBaseUriNotAbsoluteFails() $this->browser->withBase('hello'); } + public function testWithProtocolVersionFollowedByGetRequestSendsRequestWithProtocolVersion() + { + $this->browser = $this->browser->withProtocolVersion('1.0'); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('1.0', $request->getProtocolVersion()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('http://example.com/'); + } + + public function testWithProtocolVersionFollowedBySubmitRequestSendsRequestWithProtocolVersion() + { + $this->browser = $this->browser->withProtocolVersion('1.0'); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('1.0', $request->getProtocolVersion()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->submit('http://example.com/', array()); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testWithProtocolVersionInvalidThrows() + { + $this->browser->withProtocolVersion('1.2'); + } + public function testCancelGetRequestShouldCancelUnderlyingSocketConnection() { $pending = new Promise(function () { }, $this->expectCallableOnce()); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index c1bfb9f..a6af401 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -310,7 +310,7 @@ public function testReceiveStreamUntilConnectionsEndsForHttp10() $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = Block\await($this->browser->get($this->base . 'get', array()), $this->loop); + $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base . 'get', array()), $this->loop); $this->assertEquals('1.0', $response->getProtocolVersion()); $this->assertFalse($response->hasHeader('Transfer-Encoding')); @@ -463,7 +463,7 @@ public function testPostStreamClosed() $this->assertEquals('', $data['data']); } - public function testSendsHttp10ByDefault() + public function testSendsHttp11ByDefault() { $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( @@ -478,12 +478,12 @@ public function testSendsHttp10ByDefault() $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; $response = Block\await($this->browser->get($this->base), $this->loop); - $this->assertEquals('1.0', (string)$response->getBody()); + $this->assertEquals('1.1', (string)$response->getBody()); $socket->close(); } - public function testSendsExplicitHttp11Request() + public function testSendsExplicitHttp10Request() { $server = new StreamingServer(function (ServerRequestInterface $request) { return new Response( @@ -497,10 +497,8 @@ public function testSendsExplicitHttp11Request() $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $request = new Request('GET', $this->base, array(), '', '1.1'); - - $response = Block\await($this->browser->send($request), $this->loop); - $this->assertEquals('1.1', (string)$response->getBody()); + $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base), $this->loop); + $this->assertEquals('1.0', (string)$response->getBody()); $socket->close(); }