Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use HTTP/1.1 protocol version by default and add new Browser::withProtocolVersion() #162

Merged
merged 2 commits into from
May 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 49 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -517,13 +529,14 @@ $browser->submit($url, array('user' => 'test', 'password' => 'secret'));
The `send(RequestInterface $request): PromiseInterface<ResponseInterface>` 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(…);
```
Expand Down Expand Up @@ -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).
Expand Down
74 changes: 63 additions & 11 deletions src/Browser.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Browser
private $transaction;
private $messageFactory;
private $baseUri = null;
private $protocolVersion = '1.1';

/** @var LoopInterface $loop */
private $loop;
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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(…);
* ```
Expand Down Expand Up @@ -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<ResponseInterface,Exception>
*/
private function requestMayBeStreaming($method, $url, array $headers = array(), $contents = '')
{
return $this->send($this->messageFactory->request($method, $url, $headers, $contents, $this->protocolVersion));
}
}
11 changes: 6 additions & 5 deletions src/Message/MessageFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,28 @@ 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
* @param ReadableStreamInterface|string $body
* @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;
Expand Down
113 changes: 113 additions & 0 deletions tests/BrowserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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());
Expand Down
Loading