Skip to content

Commit

Permalink
Merge pull request #162 from clue-labs/http1.1
Browse files Browse the repository at this point in the history
Use HTTP/1.1 protocol version by default and add new `Browser::withProtocolVersion()`
  • Loading branch information
clue authored May 7, 2020
2 parents beaa763 + efd76ba commit 8d4468e
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 37 deletions.
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

0 comments on commit 8d4468e

Please sign in to comment.