diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c64ef6ab..b0005012 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,10 +7,13 @@ on:
jobs:
PHPUnit:
name: PHPUnit (PHP ${{ matrix.php }})
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
strategy:
matrix:
php:
+ - 8.4
+ - 8.3
+ - 8.2
- 8.1
- 8.0
- 7.4
@@ -23,11 +26,12 @@ jobs:
- 5.4
- 5.3
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: xdebug
+ ini-file: development
- run: composer install
- run: vendor/bin/phpunit --coverage-text
if: ${{ matrix.php >= 7.3 }}
@@ -36,12 +40,16 @@ jobs:
PHPUnit-hhvm:
name: PHPUnit (HHVM)
- runs-on: ubuntu-18.04
+ runs-on: ubuntu-22.04
continue-on-error: true
steps:
- - uses: actions/checkout@v2
- - uses: azjezz/setup-hhvm@v1
+ - uses: actions/checkout@v4
+ - run: cp "$(which composer)" composer.phar && ./composer.phar self-update --2.2 # downgrade Composer for HHVM
+ - name: Run hhvm composer.phar install
+ uses: docker://hhvm/hhvm:3.30-lts-latest
with:
- version: lts-3.30
- - run: hhvm $(which composer) install
- - run: hhvm vendor/bin/phpunit
+ args: hhvm composer.phar install
+ - name: Run hhvm vendor/bin/phpunit
+ uses: docker://hhvm/hhvm:3.30-lts-latest
+ with:
+ args: hhvm vendor/bin/phpunit
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 41079cdb..07a4cc66 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,123 @@
# Changelog
+## 1.11.0 (2024-11-20)
+
+* Feature: Improve PHP 8.4+ support by avoiding implicitly nullable types.
+ (#537 by @clue)
+
+* Feature: Allow underscore character in Uri host.
+ (#524 by @lulhum)
+
+* Improve test suite to fix expected error code when ext-sockets is not enabled.
+ (#539 by @WyriHaximus)
+
+## 1.10.0 (2024-03-27)
+
+* Feature: Add new PSR-7 implementation and remove dated RingCentral PSR-7 dependency.
+ (#518, #519, #520 and #522 by @clue)
+
+ This changeset allows us to maintain our own PSR-7 implementation and reduce
+ dependencies on external projects. It also improves performance slightly and
+ does not otherwise affect our public API. If you want to explicitly install
+ the old RingCentral PSR-7 dependency, you can still install it like this:
+
+ ```bash
+ composer require ringcentral/psr7
+ ```
+
+* Feature: Add new `Uri` class for new PSR-7 implementation.
+ (#521 by @clue)
+
+* Feature: Validate outgoing HTTP message headers and reject invalid messages.
+ (#523 by @clue)
+
+* Feature: Full PHP 8.3 compatibility.
+ (#508 by @clue)
+
+* Fix: Fix HTTP client to omit `Transfer-Encoding: chunked` when streaming empty request body.
+ (#516 by @clue)
+
+* Fix: Ensure connection close handler is cleaned up for each request.
+ (#515 by @WyriHaximus)
+
+* Update test suite and avoid unhandled promise rejections.
+ (#501 and #502 by @clue)
+
+## 1.9.0 (2023-04-26)
+
+This is a **SECURITY** and feature release for the 1.x series of ReactPHP's HTTP component.
+
+* Security fix: This release fixes a medium severity security issue in ReactPHP's HTTP server component
+ that affects all versions between `v0.8.0` and `v1.8.0`. All users are encouraged to upgrade immediately.
+ (CVE-2023-26044 reported and fixed by @WyriHaximus)
+
+* Feature: Support HTTP keep-alive for HTTP client (reusing persistent connections).
+ (#481, #484, #486 and #495 by @clue)
+
+ This feature offers significant performance improvements when sending many
+ requests to the same host as it avoids recreating the underlying TCP/IP
+ connection and repeating the TLS handshake for secure HTTPS requests.
+
+ ```php
+ $browser = new React\Http\Browser();
+
+ // Up to 300% faster! HTTP keep-alive is enabled by default
+ $response = React\Async\await($browser->get('https://httpbingo.org/redirect/6'));
+ assert($response instanceof Psr\Http\Message\ResponseInterface);
+ ```
+
+* Feature: Add `Request` class to represent outgoing HTTP request message.
+ (#480 by @clue)
+
+* Feature: Preserve request method and body for `307 Temporary Redirect` and `308 Permanent Redirect`.
+ (#442 by @dinooo13)
+
+* Feature: Include buffer logic to avoid dependency on reactphp/promise-stream.
+ (#482 by @clue)
+
+* Improve test suite and project setup and report failed assertions.
+ (#478 by @clue, #487 and #491 by @WyriHaximus and #475 and #479 by @SimonFrings)
+
+## 1.8.0 (2022-09-29)
+
+* Feature: Support for default request headers.
+ (#461 by @51imyy)
+
+ ```php
+ $browser = new React\Http\Browser();
+ $browser = $browser->withHeader('User-Agent', 'ACME');
+
+ $browser->get($url)->then(…);
+ ```
+
+* Feature: Forward compatibility with upcoming Promise v3.
+ (#460 by @clue)
+
+## 1.7.0 (2022-08-23)
+
+This is a **SECURITY** and feature release for the 1.x series of ReactPHP's HTTP component.
+
+* Security fix: This release fixes a medium severity security issue in ReactPHP's HTTP server component
+ that affects all versions between `v0.7.0` and `v1.6.0`. All users are encouraged to upgrade immediately.
+ Special thanks to Marco Squarcina (TU Wien) for reporting this and working with us to coordinate this release.
+ (CVE-2022-36032 reported by @lavish and fixed by @clue)
+
+* Feature: Improve HTTP server performance by ~20%, reuse syscall values for clock time and socket addresses.
+ (#457 and #467 by @clue)
+
+* Feature: Full PHP 8.2+ compatibility, refactor internal `Transaction` to avoid assigning dynamic properties.
+ (#459 by @clue and #466 by @WyriHaximus)
+
+* Feature / Fix: Allow explicit `Content-Length` response header on `HEAD` requests.
+ (#444 by @mrsimonbennett)
+
+* Minor documentation improvements.
+ (#452 by @clue, #458 by @nhedger, #448 by @jorrit and #446 by @SimonFrings)
+
+* Improve test suite, update to use new reactphp/async package instead of clue/reactphp-block,
+ skip memory tests when lowering memory limit fails and fix legacy HHVM build.
+ (#464 and #440 by @clue and #450 by @SimonFrings)
+
## 1.6.0 (2022-02-03)
* Feature: Add factory methods for common HTML/JSON/plaintext/XML response types.
@@ -10,7 +128,6 @@
$response = React\Http\Response\json(['message' => 'Hello wörld!']);
$response = React\Http\Response\plaintext("Hello wörld!\n");
$response = React\Http\Response\xml("Hello wörld!\n");
- $response = React\Http\Response\redirect('https://reactphp.org/');
```
* Feature: Expose all status code constants via `Response` class.
diff --git a/README.md b/README.md
index 154f4901..fd9ba46e 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# HTTP
-[](https://github.com/reactphp/http/actions)
+[](https://github.com/reactphp/http/actions)
[](https://packagist.org/packages/react/http)
Event-driven, streaming HTTP client and server implementation for [ReactPHP](https://reactphp.org/).
@@ -69,13 +69,17 @@ multiple concurrent HTTP requests without blocking.
* [withBase()](#withbase)
* [withProtocolVersion()](#withprotocolversion)
* [withResponseBuffer()](#withresponsebuffer)
+ * [withHeader()](#withheader)
+ * [withoutHeader()](#withoutheader)
* [React\Http\Message](#reacthttpmessage)
* [Response](#response)
* [html()](#html)
* [json()](#json)
* [plaintext()](#plaintext)
* [xml()](#xml)
+ * [Request](#request-1)
* [ServerRequest](#serverrequest)
+ * [Uri](#uri)
* [ResponseException](#responseexception)
* [React\Http\Middleware](#reacthttpmiddleware)
* [StreamingRequestMiddleware](#streamingrequestmiddleware)
@@ -340,9 +344,10 @@ $browser->get($url, $headers)->then(function (Psr\Http\Message\ResponseInterface
Any redirected requests will follow the semantics of the original request and
will include the same request headers as the original request except for those
listed below.
-If the original request contained a request body, this request body will never
-be passed to the redirected request. Accordingly, each redirected request will
-remove any `Content-Length` and `Content-Type` request headers.
+If the original request is a temporary (307) or a permanent (308) redirect, request
+body and headers will be passed to the redirected request. Otherwise, the request
+body will never be passed to the redirected request. Accordingly, each redirected
+request will remove any `Content-Length` and `Content-Type` request headers.
If the original request used HTTP authentication with an `Authorization` request
header, this request header will only be passed as part of the redirected
@@ -373,38 +378,42 @@ See also [`withFollowRedirects()`](#withfollowredirects) for more details.
As stated above, this library provides you a powerful, async API by default.
-If, however, you want to integrate this into your traditional, blocking environment,
-you should look into also using [clue/reactphp-block](https://github.com/clue/reactphp-block).
-
-The resulting blocking code could look something like this:
+You can also integrate this into your traditional, blocking environment by using
+[reactphp/async](https://github.com/reactphp/async). This allows you to simply
+await async HTTP requests like this:
```php
-use Clue\React\Block;
+use function React\Async\await;
$browser = new React\Http\Browser();
$promise = $browser->get('http://example.com/');
try {
- $response = Block\await($promise, Loop::get());
+ $response = await($promise);
// response successfully received
} catch (Exception $e) {
- // an error occured while performing the request
+ // an error occurred while performing the request
}
```
Similarly, you can also process multiple requests concurrently and await an array of `Response` objects:
```php
+use function React\Async\await;
+use function React\Promise\all;
+
$promises = array(
$browser->get('http://example.com/'),
$browser->get('http://www.example.org/'),
);
-$responses = Block\awaitAll($promises, Loop::get());
+$responses = await(all($promises));
```
-Please refer to [clue/reactphp-block](https://github.com/clue/reactphp-block#readme) for more details.
+This is made possible thanks to fibers available in PHP 8.1+ and our
+compatibility API that also works on all supported PHP versions.
+Please refer to [reactphp/async](https://github.com/reactphp/async#readme) for more details.
Keep in mind the above remark about buffering the whole response message in memory.
As an alternative, you may also see one of the following chapters for the
@@ -1300,7 +1309,7 @@ get all cookies sent with the current request.
```php
$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
- $key = 'react\php';
+ $key = 'greeting';
if (isset($request->getCookieParams()[$key])) {
$body = "Your cookie value is: " . $request->getCookieParams()[$key] . "\n";
@@ -1312,7 +1321,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf
return React\Http\Message\Response::plaintext(
"Your cookie has been set.\n"
- )->withHeader('Set-Cookie', urlencode($key) . '=' . urlencode('test;more'));
+ )->withHeader('Set-Cookie', $key . '=' . urlencode('Hello world!'));
});
```
@@ -1426,15 +1435,23 @@ may only support strings.
$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
$stream = new ThroughStream();
+ // send some data every once in a while with periodic timer
$timer = Loop::addPeriodicTimer(0.5, function () use ($stream) {
$stream->write(microtime(true) . PHP_EOL);
});
- Loop::addTimer(5, function() use ($timer, $stream) {
+ // end stream after a few seconds
+ $timeout = Loop::addTimer(5.0, function() use ($stream, $timer) {
Loop::cancelTimer($timer);
$stream->end();
});
+ // stop timer if stream is closed (such as when connection is closed)
+ $stream->on('close', function () use ($timer, $timeout) {
+ Loop::cancelTimer($timer);
+ Loop::cancelTimer($timeout);
+ });
+
return new React\Http\Message\Response(
React\Http\Message\Response::STATUS_OK,
array(
@@ -2373,6 +2390,36 @@ Notice that the [`Browser`](#browser) is an immutable object, i.e. this
method actually returns a *new* [`Browser`](#browser) instance with the
given setting applied.
+#### withHeader()
+
+The `withHeader(string $header, string $value): Browser` method can be used to
+add a request header for all following requests.
+
+```php
+$browser = $browser->withHeader('User-Agent', 'ACME');
+
+$browser->get($url)->then(…);
+```
+
+Note that the new header will overwrite any headers previously set with
+the same name (case-insensitive). Following requests will use these headers
+by default unless they are explicitly set for any requests.
+
+#### withoutHeader()
+
+The `withoutHeader(string $header): Browser` method can be used to
+remove any default request headers previously set via
+the [`withHeader()` method](#withheader).
+
+```php
+$browser = $browser->withoutHeader('User-Agent');
+
+$browser->get($url)->then(…);
+```
+
+Note that this method only affects the headers which were set with the
+method `withHeader(string $header, string $value): Browser`
+
### React\Http\Message
#### Response
@@ -2402,8 +2449,7 @@ constants with the `STATUS_*` prefix. For instance, the `200 OK` and
`404 Not Found` status codes can used as `Response::STATUS_OK` and
`Response::STATUS_NOT_FOUND` respectively.
-> Internally, this implementation builds on top of an existing incoming
- response message and only adds required streaming support. This base class is
+> Internally, this implementation builds on top of a base class which is
considered an implementation detail that may change in the future.
##### html()
@@ -2583,6 +2629,23 @@ $response = React\Http\Message\Response::xml(
)->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST);
```
+#### Request
+
+The `React\Http\Message\Request` class can be used to
+respresent an outgoing HTTP request message.
+
+This class implements the
+[PSR-7 `RequestInterface`](https://www.php-fig.org/psr/psr-7/#32-psrhttpmessagerequestinterface)
+which extends the
+[PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface).
+
+This is mostly used internally to represent each outgoing HTTP request
+message for the HTTP client implementation. Likewise, you can also use this
+class with other HTTP client implementations and for tests.
+
+> Internally, this implementation builds on top of a base class which is
+ considered an implementation detail that may change in the future.
+
#### ServerRequest
The `React\Http\Message\ServerRequest` class can be used to
@@ -2599,10 +2662,21 @@ This is mostly used internally to represent each incoming request message.
Likewise, you can also use this class in test cases to test how your web
application reacts to certain HTTP requests.
-> Internally, this implementation builds on top of an existing outgoing
- request message and only adds required server methods. This base class is
+> Internally, this implementation builds on top of a base class which is
considered an implementation detail that may change in the future.
+#### Uri
+
+The `React\Http\Message\Uri` class can be used to
+respresent a URI (or URL).
+
+This class implements the
+[PSR-7 `UriInterface`](https://www.php-fig.org/psr/psr-7/#35-psrhttpmessageuriinterface).
+
+This is mostly used internally to represent the URI of each HTTP request
+message for our HTTP client and server implementations. Likewise, you may
+also use this class with other HTTP implementations and for tests.
+
#### ResponseException
The `React\Http\Message\ResponseException` is an `Exception` sub-class that will be used to reject
@@ -2912,7 +2986,7 @@ This project follows [SemVer](https://semver.org/).
This will install the latest supported version:
```bash
-$ composer require react/http:^1.6
+composer require react/http:^1.11
```
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
@@ -2928,13 +3002,13 @@ To run the test suite, you first need to clone this repo and then install all
dependencies [through Composer](https://getcomposer.org/):
```bash
-$ composer install
+composer install
```
To run the test suite, go to the project root and run:
```bash
-$ vendor/bin/phpunit
+vendor/bin/phpunit
```
The test suite also contains a number of functional integration tests that rely
@@ -2942,7 +3016,7 @@ on a stable internet connection.
If you do not want to run these, they can simply be skipped like this:
```bash
-$ vendor/bin/phpunit --exclude-group internet
+vendor/bin/phpunit --exclude-group internet
```
## License
diff --git a/composer.json b/composer.json
index 4c9a0383..4234210a 100644
--- a/composer.json
+++ b/composer.json
@@ -31,23 +31,27 @@
"fig/http-message-util": "^1.1",
"psr/http-message": "^1.0",
"react/event-loop": "^1.2",
- "react/promise": "^2.3 || ^1.2.1",
- "react/promise-stream": "^1.1",
- "react/socket": "^1.9",
- "react/stream": "^1.2",
- "ringcentral/psr7": "^1.2"
+ "react/promise": "^3.2 || ^2.3 || ^1.2.1",
+ "react/socket": "^1.16",
+ "react/stream": "^1.4"
},
"require-dev": {
- "clue/block-react": "^1.5",
- "clue/http-proxy-react": "^1.7",
- "clue/reactphp-ssh-proxy": "^1.3",
- "clue/socks-react": "^1.3",
- "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35"
+ "clue/http-proxy-react": "^1.8",
+ "clue/reactphp-ssh-proxy": "^1.4",
+ "clue/socks-react": "^1.4",
+ "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
+ "react/async": "^4.2 || ^3 || ^2",
+ "react/promise-stream": "^1.4",
+ "react/promise-timer": "^1.11"
},
"autoload": {
- "psr-4": { "React\\Http\\": "src" }
+ "psr-4": {
+ "React\\Http\\": "src/"
+ }
},
"autoload-dev": {
- "psr-4": { "React\\Tests\\Http\\": "tests" }
+ "psr-4": {
+ "React\\Tests\\Http\\": "tests/"
+ }
}
}
diff --git a/examples/01-client-get-request.php b/examples/01-client-get-request.php
index 34a79bbb..278f6597 100644
--- a/examples/01-client-get-request.php
+++ b/examples/01-client-get-request.php
@@ -8,7 +8,7 @@
$client = new Browser();
$client->get('http://google.com/')->then(function (ResponseInterface $response) {
- var_dump($response->getHeaders(), (string)$response->getBody());
+ echo (string) $response->getBody();
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
diff --git a/examples/02-client-concurrent-requests.php b/examples/02-client-concurrent-requests.php
index 7b1b77a0..e372515c 100644
--- a/examples/02-client-concurrent-requests.php
+++ b/examples/02-client-concurrent-requests.php
@@ -8,19 +8,19 @@
$client = new Browser();
$client->head('http://www.github.com/clue/http-react')->then(function (ResponseInterface $response) {
- var_dump($response->getHeaders(), (string)$response->getBody());
+ echo (string) $response->getBody();
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
$client->get('http://google.com/')->then(function (ResponseInterface $response) {
- var_dump($response->getHeaders(), (string)$response->getBody());
+ echo (string) $response->getBody();
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
$client->get('http://www.lueck.tv/psocksd')->then(function (ResponseInterface $response) {
- var_dump($response->getHeaders(), (string)$response->getBody());
+ echo (string) $response->getBody();
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
diff --git a/examples/03-client-request-any.php b/examples/03-client-request-any.php
index 0c96b684..d7558bd6 100644
--- a/examples/03-client-request-any.php
+++ b/examples/03-client-request-any.php
@@ -12,10 +12,10 @@
$promises = array(
$client->head('http://www.github.com/clue/http-react'),
- $client->get('https://httpbin.org/'),
+ $client->get('https://httpbingo.org/'),
$client->get('https://google.com/'),
$client->get('http://www.lueck.tv/psocksd'),
- $client->get('http://www.httpbin.org/absolute-redirect/5')
+ $client->get('http://httpbingo.org/absolute-redirect/5')
);
React\Promise\any($promises)->then(function (ResponseInterface $response) use ($promises) {
diff --git a/examples/04-client-post-json.php b/examples/04-client-post-json.php
index b01ada13..18fa596d 100644
--- a/examples/04-client-post-json.php
+++ b/examples/04-client-post-json.php
@@ -16,13 +16,13 @@
);
$client->post(
- 'https://httpbin.org/post',
+ 'https://httpbingo.org/post',
array(
'Content-Type' => 'application/json'
),
json_encode($data)
)->then(function (ResponseInterface $response) {
- echo (string)$response->getBody();
+ echo (string) $response->getBody();
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
diff --git a/examples/05-client-put-xml.php b/examples/05-client-put-xml.php
index 231e2ca4..10ee46fc 100644
--- a/examples/05-client-put-xml.php
+++ b/examples/05-client-put-xml.php
@@ -13,13 +13,13 @@
$child->name = 'Christian Lück';
$client->put(
- 'https://httpbin.org/put',
+ 'https://httpbingo.org/put',
array(
'Content-Type' => 'text/xml'
),
$xml->asXML()
)->then(function (ResponseInterface $response) {
- echo (string)$response->getBody();
+ echo (string) $response->getBody();
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
diff --git a/examples/11-client-http-proxy.php b/examples/11-client-http-proxy.php
index f450fbc2..ec7fc2b6 100644
--- a/examples/11-client-http-proxy.php
+++ b/examples/11-client-http-proxy.php
@@ -25,7 +25,7 @@
// demo fetching HTTP headers (or bail out otherwise)
$browser->get('https://www.google.com/')->then(function (ResponseInterface $response) {
- echo RingCentral\Psr7\str($response);
+ echo (string) $response->getBody();
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
diff --git a/examples/12-client-socks-proxy.php b/examples/12-client-socks-proxy.php
index ecedf242..8c525509 100644
--- a/examples/12-client-socks-proxy.php
+++ b/examples/12-client-socks-proxy.php
@@ -25,7 +25,7 @@
// demo fetching HTTP headers (or bail out otherwise)
$browser->get('https://www.google.com/')->then(function (ResponseInterface $response) {
- echo RingCentral\Psr7\str($response);
+ echo (string) $response->getBody();
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
diff --git a/examples/13-client-ssh-proxy.php b/examples/13-client-ssh-proxy.php
index 64d0c282..93e6e256 100644
--- a/examples/13-client-ssh-proxy.php
+++ b/examples/13-client-ssh-proxy.php
@@ -21,7 +21,7 @@
// demo fetching HTTP headers (or bail out otherwise)
$browser->get('https://www.google.com/')->then(function (ResponseInterface $response) {
- echo RingCentral\Psr7\str($response);
+ echo (string) $response->getBody();
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
diff --git a/examples/14-client-unix-domain-sockets.php b/examples/14-client-unix-domain-sockets.php
index e9718141..5af0394d 100644
--- a/examples/14-client-unix-domain-sockets.php
+++ b/examples/14-client-unix-domain-sockets.php
@@ -4,7 +4,6 @@
use React\Http\Browser;
use React\Socket\FixedUriConnector;
use React\Socket\UnixConnector;
-use RingCentral\Psr7;
require __DIR__ . '/../vendor/autoload.php';
@@ -18,7 +17,7 @@
// demo fetching HTTP headers (or bail out otherwise)
$browser->get('http://localhost/info')->then(function (ResponseInterface $response) {
- echo Psr7\str($response);
+ echo (string) $response->getBody();
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
diff --git a/examples/21-client-request-streaming-to-stdout.php b/examples/21-client-request-streaming-to-stdout.php
index 2f24d035..b3cbbe39 100644
--- a/examples/21-client-request-streaming-to-stdout.php
+++ b/examples/21-client-request-streaming-to-stdout.php
@@ -4,7 +4,6 @@
use Psr\Http\Message\ResponseInterface;
use React\Stream\ReadableStreamInterface;
use React\Stream\WritableResourceStream;
-use RingCentral\Psr7;
require __DIR__ . '/../vendor/autoload.php';
@@ -22,7 +21,7 @@
$info->write('Requesting ' . $url . '…' . PHP_EOL);
$client->requestStreaming('GET', $url)->then(function (ResponseInterface $response) use ($info, $out) {
- $info->write('Received' . PHP_EOL . Psr7\str($response));
+ $info->write('Received ' . $response->getStatusCode() . ' ' . $response->getReasonPhrase() . PHP_EOL);
$body = $response->getBody();
assert($body instanceof ReadableStreamInterface);
diff --git a/examples/22-client-stream-upload-from-stdin.php b/examples/22-client-stream-upload-from-stdin.php
index b00fbc5e..f0a68c5f 100644
--- a/examples/22-client-stream-upload-from-stdin.php
+++ b/examples/22-client-stream-upload-from-stdin.php
@@ -3,7 +3,6 @@
use Psr\Http\Message\ResponseInterface;
use React\Http\Browser;
use React\Stream\ReadableResourceStream;
-use RingCentral\Psr7;
require __DIR__ . '/../vendor/autoload.php';
@@ -16,11 +15,11 @@
$in = new ReadableResourceStream(STDIN);
-$url = isset($argv[1]) ? $argv[1] : 'https://httpbin.org/post';
+$url = isset($argv[1]) ? $argv[1] : 'https://httpbingo.org/post';
echo 'Sending STDIN as POST to ' . $url . '…' . PHP_EOL;
-$client->post($url, array(), $in)->then(function (ResponseInterface $response) {
- echo 'Received' . PHP_EOL . Psr7\str($response);
+$client->post($url, array('Content-Type' => 'text/plain'), $in)->then(function (ResponseInterface $response) {
+ echo (string) $response->getBody();
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php
index 796da24d..b5e68862 100644
--- a/examples/55-server-cookie-handling.php
+++ b/examples/55-server-cookie-handling.php
@@ -3,7 +3,7 @@
require __DIR__ . '/../vendor/autoload.php';
$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
- $key = 'react\php';
+ $key = 'greeting';
if (isset($request->getCookieParams()[$key])) {
$body = "Your cookie value is: " . $request->getCookieParams()[$key] . "\n";
@@ -15,7 +15,7 @@
return React\Http\Message\Response::plaintext(
"Your cookie has been set.\n"
- )->withHeader('Set-Cookie', urlencode($key) . '=' . urlencode('test;more'));
+ )->withHeader('Set-Cookie', $key . '=' . urlencode('Hello world!'));
});
$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0');
diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php
index cf65a3bf..9d12461a 100644
--- a/examples/58-server-stream-response.php
+++ b/examples/58-server-stream-response.php
@@ -18,14 +18,16 @@
$stream->write(microtime(true) . PHP_EOL);
});
- // demo for ending stream after a few seconds
- Loop::addTimer(5.0, function() use ($stream) {
+ // end stream after a few seconds
+ $timeout = Loop::addTimer(5.0, function() use ($stream, $timer) {
+ Loop::cancelTimer($timer);
$stream->end();
});
// stop timer if stream is closed (such as when connection is closed)
- $stream->on('close', function () use ($timer) {
+ $stream->on('close', function () use ($timer, $timeout) {
Loop::cancelTimer($timer);
+ Loop::cancelTimer($timeout);
});
return new Response(
diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php
index cf63c4ae..de9fa10b 100644
--- a/examples/71-server-http-proxy.php
+++ b/examples/71-server-http-proxy.php
@@ -28,7 +28,7 @@
// left up as an exercise: use an HTTP client to send the outgoing request
// and forward the incoming response to the original client request
return React\Http\Message\Response::plaintext(
- RingCentral\Psr7\str($outgoing)
+ $outgoing->getMethod() . ' ' . $outgoing->getRequestTarget() . ' HTTP/' . $outgoing->getProtocolVersion() . "\r\n\r\n" . (string) $outgoing->getBody()
);
});
diff --git a/examples/91-client-benchmark-download.php b/examples/91-client-benchmark-download.php
index 49693baf..712d9f10 100644
--- a/examples/91-client-benchmark-download.php
+++ b/examples/91-client-benchmark-download.php
@@ -1,7 +1,7 @@
requestStreaming('GET', $url)->then(function (ResponseInterface $response) {
- echo 'Headers received' . PHP_EOL;
- echo RingCentral\Psr7\str($response);
+ echo 'Received ' . $response->getStatusCode() . ' ' . $response->getReasonPhrase() . PHP_EOL;
$stream = $response->getBody();
assert($stream instanceof ReadableStreamInterface);
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 93a36f6b..ac542e77 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,8 +1,8 @@
-
-
+./src/
+
+
+
+
+
+
+
+
diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy
index fbb43e85..89161168 100644
--- a/phpunit.xml.legacy
+++ b/phpunit.xml.legacy
@@ -1,6 +1,6 @@
-
+
./src/
+
+
+
+
+
+
+
+
diff --git a/src/Browser.php b/src/Browser.php
index 72847f66..9da0dcab 100644
--- a/src/Browser.php
+++ b/src/Browser.php
@@ -3,14 +3,14 @@
namespace React\Http;
use Psr\Http\Message\ResponseInterface;
-use RingCentral\Psr7\Request;
-use RingCentral\Psr7\Uri;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
-use React\Http\Io\ReadableBodyStream;
use React\Http\Io\Sender;
use React\Http\Io\Transaction;
+use React\Http\Message\Request;
+use React\Http\Message\Uri;
use React\Promise\PromiseInterface;
+use React\Socket\Connector;
use React\Socket\ConnectorInterface;
use React\Stream\ReadableStreamInterface;
use InvalidArgumentException;
@@ -23,6 +23,9 @@ class Browser
private $transaction;
private $baseUrl;
private $protocolVersion = '1.1';
+ private $defaultHeaders = array(
+ 'User-Agent' => 'ReactPHP/1'
+ );
/**
* The `Browser` is responsible for sending HTTP requests to your HTTP server
@@ -86,7 +89,7 @@ public function __construct($connector = null, $loop = null)
$loop = $loop ?: Loop::get();
$this->transaction = new Transaction(
- Sender::createFromLoop($loop, $connector),
+ Sender::createFromLoop($loop, $connector ?: new Connector(array(), $loop)),
$loop
);
}
@@ -342,7 +345,7 @@ public function delete($url, array $headers = array(), $body = '')
* @param string $url URL for the request
* @param array $headers Additional request headers
* @param string|ReadableStreamInterface $body HTTP request body contents
- * @return PromiseInterface
+ * @return PromiseInterface
*/
public function request($method, $url, array $headers = array(), $body = '')
{
@@ -415,7 +418,7 @@ public function request($method, $url, array $headers = array(), $body = '')
* @param string $url URL for the request
* @param array $headers Additional request headers
* @param string|ReadableStreamInterface $body HTTP request body contents
- * @return PromiseInterface
+ * @return PromiseInterface
*/
public function requestStreaming($method, $url, $headers = array(), $body = '')
{
@@ -725,6 +728,62 @@ public function withResponseBuffer($maximumSize)
));
}
+ /**
+ * Add a request header for all following requests.
+ *
+ * ```php
+ * $browser = $browser->withHeader('User-Agent', 'ACME');
+ *
+ * $browser->get($url)->then(…);
+ * ```
+ *
+ * Note that the new header will overwrite any headers previously set with
+ * the same name (case-insensitive). Following requests will use these headers
+ * by default unless they are explicitly set for any requests.
+ *
+ * @param string $header
+ * @param string $value
+ * @return Browser
+ */
+ public function withHeader($header, $value)
+ {
+ $browser = $this->withoutHeader($header);
+ $browser->defaultHeaders[$header] = $value;
+
+ return $browser;
+ }
+
+ /**
+ * Remove any default request headers previously set via
+ * the [`withHeader()` method](#withheader).
+ *
+ * ```php
+ * $browser = $browser->withoutHeader('User-Agent');
+ *
+ * $browser->get($url)->then(…);
+ * ```
+ *
+ * Note that this method only affects the headers which were set with the
+ * method `withHeader(string $header, string $value): Browser`
+ *
+ * @param string $header
+ * @return Browser
+ */
+ public function withoutHeader($header)
+ {
+ $browser = clone $this;
+
+ /** @var string|int $key */
+ foreach (\array_keys($browser->defaultHeaders) as $key) {
+ if (\strcasecmp($key, $header) === 0) {
+ unset($browser->defaultHeaders[$key]);
+ break;
+ }
+ }
+
+ return $browser;
+ }
+
/**
* Changes the [options](#options) to use:
*
@@ -770,17 +829,26 @@ private function withOptions(array $options)
* @param string $url
* @param array $headers
* @param string|ReadableStreamInterface $body
- * @return PromiseInterface
+ * @return PromiseInterface
*/
private function requestMayBeStreaming($method, $url, array $headers = array(), $body = '')
{
if ($this->baseUrl !== null) {
// ensure we're actually below the base URL
- $url = Uri::resolve($this->baseUrl, $url);
+ $url = Uri::resolve($this->baseUrl, new Uri($url));
}
- if ($body instanceof ReadableStreamInterface) {
- $body = new ReadableBodyStream($body);
+ foreach ($this->defaultHeaders as $key => $value) {
+ $explicitHeaderExists = false;
+ foreach (\array_keys($headers) as $headerKey) {
+ if (\strcasecmp($headerKey, $key) === 0) {
+ $explicitHeaderExists = true;
+ break;
+ }
+ }
+ if (!$explicitHeaderExists) {
+ $headers[$key] = $value;
+ }
}
return $this->transaction->send(
diff --git a/src/Client/Client.php b/src/Client/Client.php
index 7a97349c..7a5180ab 100644
--- a/src/Client/Client.php
+++ b/src/Client/Client.php
@@ -2,30 +2,26 @@
namespace React\Http\Client;
-use React\EventLoop\LoopInterface;
-use React\Socket\ConnectorInterface;
-use React\Socket\Connector;
+use Psr\Http\Message\RequestInterface;
+use React\Http\Io\ClientConnectionManager;
+use React\Http\Io\ClientRequestStream;
/**
* @internal
*/
class Client
{
- private $connector;
+ /** @var ClientConnectionManager */
+ private $connectionManager;
- public function __construct(LoopInterface $loop, ConnectorInterface $connector = null)
+ public function __construct(ClientConnectionManager $connectionManager)
{
- if ($connector === null) {
- $connector = new Connector(array(), $loop);
- }
-
- $this->connector = $connector;
+ $this->connectionManager = $connectionManager;
}
- 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->connectionManager, $request);
}
}
diff --git a/src/Client/Request.php b/src/Client/Request.php
deleted file mode 100644
index 51e03313..00000000
--- a/src/Client/Request.php
+++ /dev/null
@@ -1,237 +0,0 @@
-connector = $connector;
- $this->requestData = $requestData;
- }
-
- public function isWritable()
- {
- return self::STATE_END > $this->state && !$this->ended;
- }
-
- private function writeHead()
- {
- $this->state = self::STATE_WRITING_HEAD;
-
- $requestData = $this->requestData;
- $streamRef = &$this->stream;
- $stateRef = &$this->state;
- $pendingWrites = &$this->pendingWrites;
- $that = $this;
-
- $promise = $this->connect();
- $promise->then(
- function (ConnectionInterface $stream) use ($requestData, &$streamRef, &$stateRef, &$pendingWrites, $that) {
- $streamRef = $stream;
-
- $stream->on('drain', array($that, 'handleDrain'));
- $stream->on('data', array($that, 'handleData'));
- $stream->on('end', array($that, 'handleEnd'));
- $stream->on('error', array($that, 'handleError'));
- $stream->on('close', array($that, 'handleClose'));
-
- $headers = (string) $requestData;
-
- $more = $stream->write($headers . $pendingWrites);
-
- $stateRef = Request::STATE_HEAD_WRITTEN;
-
- // clear pending writes if non-empty
- if ($pendingWrites !== '') {
- $pendingWrites = '';
-
- if ($more) {
- $that->emit('drain');
- }
- }
- },
- array($this, 'closeError')
- );
-
- $this->on('close', function() use ($promise) {
- $promise->cancel();
- });
- }
-
- public function write($data)
- {
- if (!$this->isWritable()) {
- return false;
- }
-
- // write directly to connection stream if already available
- if (self::STATE_HEAD_WRITTEN <= $this->state) {
- return $this->stream->write($data);
- }
-
- // otherwise buffer and try to establish connection
- $this->pendingWrites .= $data;
- if (self::STATE_WRITING_HEAD > $this->state) {
- $this->writeHead();
- }
-
- return false;
- }
-
- public function end($data = null)
- {
- if (!$this->isWritable()) {
- return;
- }
-
- if (null !== $data) {
- $this->write($data);
- } else if (self::STATE_WRITING_HEAD > $this->state) {
- $this->writeHead();
- }
-
- $this->ended = true;
- }
-
- /** @internal */
- public function handleDrain()
- {
- $this->emit('drain');
- }
-
- /** @internal */
- public function handleData($data)
- {
- $this->buffer .= $data;
-
- // buffer until double CRLF (or double LF for compatibility with legacy servers)
- if (false !== strpos($this->buffer, "\r\n\r\n") || false !== strpos($this->buffer, "\n\n")) {
- try {
- $response = gPsr\parse_response($this->buffer);
- $bodyChunk = (string) $response->getBody();
- } catch (\InvalidArgumentException $exception) {
- $this->emit('error', array($exception));
- }
-
- $this->buffer = null;
-
- $this->stream->removeListener('drain', array($this, 'handleDrain'));
- $this->stream->removeListener('data', array($this, 'handleData'));
- $this->stream->removeListener('end', array($this, 'handleEnd'));
- $this->stream->removeListener('error', array($this, 'handleError'));
- $this->stream->removeListener('close', array($this, 'handleClose'));
-
- if (!isset($response)) {
- return;
- }
-
- $this->stream->on('close', array($this, 'handleClose'));
-
- $this->emit('response', array($response, $this->stream));
-
- $this->stream->emit('data', array($bodyChunk));
- }
- }
-
- /** @internal */
- public function handleEnd()
- {
- $this->closeError(new \RuntimeException(
- "Connection ended before receiving response"
- ));
- }
-
- /** @internal */
- public function handleError(\Exception $error)
- {
- $this->closeError(new \RuntimeException(
- "An error occurred in the underlying stream",
- 0,
- $error
- ));
- }
-
- /** @internal */
- public function handleClose()
- {
- $this->close();
- }
-
- /** @internal */
- public function closeError(\Exception $error)
- {
- if (self::STATE_END <= $this->state) {
- return;
- }
- $this->emit('error', array($error));
- $this->close();
- }
-
- public function close()
- {
- if (self::STATE_END <= $this->state) {
- return;
- }
-
- $this->state = self::STATE_END;
- $this->pendingWrites = '';
-
- if ($this->stream) {
- $this->stream->close();
- }
-
- $this->emit('close');
- $this->removeAllListeners();
- }
-
- protected function connect()
- {
- $scheme = $this->requestData->getScheme();
- if ($scheme !== 'https' && $scheme !== 'http') {
- return Promise\reject(
- new \InvalidArgumentException('Invalid request URL given')
- );
- }
-
- $host = $this->requestData->getHost();
- $port = $this->requestData->getPort();
-
- if ($scheme === 'https') {
- $host = 'tls://' . $host;
- }
-
- return $this->connector
- ->connect($host . ':' . $port);
- }
-}
diff --git a/src/Client/RequestData.php b/src/Client/RequestData.php
deleted file mode 100644
index a5908a08..00000000
--- a/src/Client/RequestData.php
+++ /dev/null
@@ -1,128 +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,
- 'User-Agent' => 'ReactPHP/1',
- ),
- $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(https://github.com/$this-%3Eurl,%20PHP_URL_SCHEME);
- }
-
- public function getHost()
- {
- return parse_url(https://github.com/$this-%3Eurl,%20PHP_URL_HOST);
- }
-
- public function getPort()
- {
- return (int) parse_url(https://github.com/$this-%3Eurl,%20PHP_URL_PORT) ?: $this->getDefaultPort();
- }
-
- public function getDefaultPort()
- {
- return ('https' === $this->getScheme()) ? 443 : 80;
- }
-
- public function getPath()
- {
- $path = parse_url(https://github.com/$this-%3Eurl,%20PHP_URL_PATH);
- $queryString = parse_url(https://github.com/$this-%3Eurl,%20PHP_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(https://github.com/$this-%3Eurl);
-
- 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/AbstractMessage.php b/src/Io/AbstractMessage.php
new file mode 100644
index 00000000..a0706bb1
--- /dev/null
+++ b/src/Io/AbstractMessage.php
@@ -0,0 +1,172 @@
+@,;:\\\"\/\[\]?={}\x00-\x20\x7F]++):[\x20\x09]*+((?:[\x20\x09]*+[\x21-\x7E\x80-\xFF]++)*+)[\x20\x09]*+[\r]?+\n/m';
+
+ /** @var array */
+ private $headers = array();
+
+ /** @var array */
+ private $headerNamesLowerCase = array();
+
+ /** @var string */
+ private $protocolVersion;
+
+ /** @var StreamInterface */
+ private $body;
+
+ /**
+ * @param string $protocolVersion
+ * @param array $headers
+ * @param StreamInterface $body
+ */
+ protected function __construct($protocolVersion, array $headers, StreamInterface $body)
+ {
+ foreach ($headers as $name => $value) {
+ if ($value !== array()) {
+ if (\is_array($value)) {
+ foreach ($value as &$one) {
+ $one = (string) $one;
+ }
+ } else {
+ $value = array((string) $value);
+ }
+
+ $lower = \strtolower($name);
+ if (isset($this->headerNamesLowerCase[$lower])) {
+ $value = \array_merge($this->headers[$this->headerNamesLowerCase[$lower]], $value);
+ unset($this->headers[$this->headerNamesLowerCase[$lower]]);
+ }
+
+ $this->headers[$name] = $value;
+ $this->headerNamesLowerCase[$lower] = $name;
+ }
+ }
+
+ $this->protocolVersion = (string) $protocolVersion;
+ $this->body = $body;
+ }
+
+ public function getProtocolVersion()
+ {
+ return $this->protocolVersion;
+ }
+
+ public function withProtocolVersion($version)
+ {
+ if ((string) $version === $this->protocolVersion) {
+ return $this;
+ }
+
+ $message = clone $this;
+ $message->protocolVersion = (string) $version;
+
+ return $message;
+ }
+
+ public function getHeaders()
+ {
+ return $this->headers;
+ }
+
+ public function hasHeader($name)
+ {
+ return isset($this->headerNamesLowerCase[\strtolower($name)]);
+ }
+
+ public function getHeader($name)
+ {
+ $lower = \strtolower($name);
+ return isset($this->headerNamesLowerCase[$lower]) ? $this->headers[$this->headerNamesLowerCase[$lower]] : array();
+ }
+
+ public function getHeaderLine($name)
+ {
+ return \implode(', ', $this->getHeader($name));
+ }
+
+ public function withHeader($name, $value)
+ {
+ if ($value === array()) {
+ return $this->withoutHeader($name);
+ } elseif (\is_array($value)) {
+ foreach ($value as &$one) {
+ $one = (string) $one;
+ }
+ } else {
+ $value = array((string) $value);
+ }
+
+ $lower = \strtolower($name);
+ if (isset($this->headerNamesLowerCase[$lower]) && $this->headerNamesLowerCase[$lower] === (string) $name && $this->headers[$this->headerNamesLowerCase[$lower]] === $value) {
+ return $this;
+ }
+
+ $message = clone $this;
+ if (isset($message->headerNamesLowerCase[$lower])) {
+ unset($message->headers[$message->headerNamesLowerCase[$lower]]);
+ }
+
+ $message->headers[$name] = $value;
+ $message->headerNamesLowerCase[$lower] = $name;
+
+ return $message;
+ }
+
+ public function withAddedHeader($name, $value)
+ {
+ if ($value === array()) {
+ return $this;
+ }
+
+ return $this->withHeader($name, \array_merge($this->getHeader($name), \is_array($value) ? $value : array($value)));
+ }
+
+ public function withoutHeader($name)
+ {
+ $lower = \strtolower($name);
+ if (!isset($this->headerNamesLowerCase[$lower])) {
+ return $this;
+ }
+
+ $message = clone $this;
+ unset($message->headers[$message->headerNamesLowerCase[$lower]], $message->headerNamesLowerCase[$lower]);
+
+ return $message;
+ }
+
+ public function getBody()
+ {
+ return $this->body;
+ }
+
+ public function withBody(StreamInterface $body)
+ {
+ if ($body === $this->body) {
+ return $this;
+ }
+
+ $message = clone $this;
+ $message->body = $body;
+
+ return $message;
+ }
+}
diff --git a/src/Io/AbstractRequest.php b/src/Io/AbstractRequest.php
new file mode 100644
index 00000000..f32307f7
--- /dev/null
+++ b/src/Io/AbstractRequest.php
@@ -0,0 +1,156 @@
+ $headers
+ * @param StreamInterface $body
+ * @param string unknown $protocolVersion
+ */
+ protected function __construct(
+ $method,
+ $uri,
+ array $headers,
+ StreamInterface $body,
+ $protocolVersion
+ ) {
+ if (\is_string($uri)) {
+ $uri = new Uri($uri);
+ } elseif (!$uri instanceof UriInterface) {
+ throw new \InvalidArgumentException(
+ 'Argument #2 ($uri) expected string|Psr\Http\Message\UriInterface'
+ );
+ }
+
+ // assign default `Host` request header from URI unless already given explicitly
+ $host = $uri->getHost();
+ if ($host !== '') {
+ foreach ($headers as $name => $value) {
+ if (\strtolower($name) === 'host' && $value !== array()) {
+ $host = '';
+ break;
+ }
+ }
+ if ($host !== '') {
+ $port = $uri->getPort();
+ if ($port !== null && (!($port === 80 && $uri->getScheme() === 'http') || !($port === 443 && $uri->getScheme() === 'https'))) {
+ $host .= ':' . $port;
+ }
+
+ $headers = array('Host' => $host) + $headers;
+ }
+ }
+
+ parent::__construct($protocolVersion, $headers, $body);
+
+ $this->method = $method;
+ $this->uri = $uri;
+ }
+
+ public function getRequestTarget()
+ {
+ if ($this->requestTarget !== null) {
+ return $this->requestTarget;
+ }
+
+ $target = $this->uri->getPath();
+ if ($target === '') {
+ $target = '/';
+ }
+ if (($query = $this->uri->getQuery()) !== '') {
+ $target .= '?' . $query;
+ }
+
+ return $target;
+ }
+
+ public function withRequestTarget($requestTarget)
+ {
+ if ((string) $requestTarget === $this->requestTarget) {
+ return $this;
+ }
+
+ $request = clone $this;
+ $request->requestTarget = (string) $requestTarget;
+
+ return $request;
+ }
+
+ public function getMethod()
+ {
+ return $this->method;
+ }
+
+ public function withMethod($method)
+ {
+ if ((string) $method === $this->method) {
+ return $this;
+ }
+
+ $request = clone $this;
+ $request->method = (string) $method;
+
+ return $request;
+ }
+
+ public function getUri()
+ {
+ return $this->uri;
+ }
+
+ public function withUri(UriInterface $uri, $preserveHost = false)
+ {
+ if ($uri === $this->uri) {
+ return $this;
+ }
+
+ $request = clone $this;
+ $request->uri = $uri;
+
+ $host = $uri->getHost();
+ $port = $uri->getPort();
+ if ($port !== null && $host !== '' && (!($port === 80 && $uri->getScheme() === 'http') || !($port === 443 && $uri->getScheme() === 'https'))) {
+ $host .= ':' . $port;
+ }
+
+ // update `Host` request header if URI contains a new host and `$preserveHost` is false
+ if ($host !== '' && (!$preserveHost || $request->getHeaderLine('Host') === '')) {
+ // first remove all headers before assigning `Host` header to ensure it always comes first
+ foreach (\array_keys($request->getHeaders()) as $name) {
+ $request = $request->withoutHeader($name);
+ }
+
+ // add `Host` header first, then all other original headers
+ $request = $request->withHeader('Host', $host);
+ foreach ($this->withoutHeader('Host')->getHeaders() as $name => $value) {
+ $request = $request->withHeader($name, $value);
+ }
+ }
+
+ return $request;
+ }
+}
diff --git a/src/Io/ClientConnectionManager.php b/src/Io/ClientConnectionManager.php
new file mode 100644
index 00000000..faac98b6
--- /dev/null
+++ b/src/Io/ClientConnectionManager.php
@@ -0,0 +1,137 @@
+connector = $connector;
+ $this->loop = $loop;
+ }
+
+ /**
+ * @return PromiseInterface
+ */
+ public function connect(UriInterface $uri)
+ {
+ $scheme = $uri->getScheme();
+ if ($scheme !== 'https' && $scheme !== 'http') {
+ return \React\Promise\reject(new \InvalidArgumentException(
+ 'Invalid request URL given'
+ ));
+ }
+
+ $port = $uri->getPort();
+ if ($port === null) {
+ $port = $scheme === 'https' ? 443 : 80;
+ }
+ $uri = ($scheme === 'https' ? 'tls://' : '') . $uri->getHost() . ':' . $port;
+
+ // Reuse idle connection for same URI if available
+ foreach ($this->idleConnections as $id => $connection) {
+ if ($this->idleUris[$id] === $uri) {
+ assert($this->idleStreamHandlers[$id] instanceof \Closure);
+ $connection->removeListener('close', $this->idleStreamHandlers[$id]);
+ $connection->removeListener('data', $this->idleStreamHandlers[$id]);
+ $connection->removeListener('error', $this->idleStreamHandlers[$id]);
+
+ assert($this->idleTimers[$id] instanceof TimerInterface);
+ $this->loop->cancelTimer($this->idleTimers[$id]);
+ unset($this->idleUris[$id], $this->idleConnections[$id], $this->idleTimers[$id], $this->idleStreamHandlers[$id]);
+
+ return \React\Promise\resolve($connection);
+ }
+ }
+
+ // Create new connection if no idle connection to same URI is available
+ return $this->connector->connect($uri);
+ }
+
+ /**
+ * Hands back an idle connection to the connection manager for possible future reuse.
+ *
+ * @return void
+ */
+ public function keepAlive(UriInterface $uri, ConnectionInterface $connection)
+ {
+ $scheme = $uri->getScheme();
+ assert($scheme === 'https' || $scheme === 'http');
+
+ $port = $uri->getPort();
+ if ($port === null) {
+ $port = $scheme === 'https' ? 443 : 80;
+ }
+
+ $this->idleUris[] = ($scheme === 'https' ? 'tls://' : '') . $uri->getHost() . ':' . $port;
+ $this->idleConnections[] = $connection;
+
+ $that = $this;
+ $cleanUp = function () use ($connection, $that) {
+ // call public method to support legacy PHP 5.3
+ $that->cleanUpConnection($connection);
+ };
+
+ // clean up and close connection when maximum time to keep-alive idle connection has passed
+ $this->idleTimers[] = $this->loop->addTimer($this->maximumTimeToKeepAliveIdleConnection, $cleanUp);
+
+ // clean up and close connection when unexpected close/data/error event happens during idle time
+ $this->idleStreamHandlers[] = $cleanUp;
+ $connection->on('close', $cleanUp);
+ $connection->on('data', $cleanUp);
+ $connection->on('error', $cleanUp);
+ }
+
+ /**
+ * @internal
+ * @return void
+ */
+ public function cleanUpConnection(ConnectionInterface $connection) // private (PHP 5.4+)
+ {
+ $id = \array_search($connection, $this->idleConnections, true);
+ if ($id === false) {
+ return;
+ }
+
+ assert(\is_int($id));
+ assert($this->idleTimers[$id] instanceof TimerInterface);
+ $this->loop->cancelTimer($this->idleTimers[$id]);
+ unset($this->idleUris[$id], $this->idleConnections[$id], $this->idleTimers[$id], $this->idleStreamHandlers[$id]);
+
+ $connection->close();
+ }
+}
diff --git a/src/Io/ClientRequestState.php b/src/Io/ClientRequestState.php
new file mode 100644
index 00000000..73a63a14
--- /dev/null
+++ b/src/Io/ClientRequestState.php
@@ -0,0 +1,16 @@
+connectionManager = $connectionManager;
+ $this->request = $request;
+ }
+
+ public function isWritable()
+ {
+ return self::STATE_END > $this->state && !$this->ended;
+ }
+
+ private function writeHead()
+ {
+ $this->state = self::STATE_WRITING_HEAD;
+
+ $expected = 0;
+ $headers = "{$this->request->getMethod()} {$this->request->getRequestTarget()} HTTP/{$this->request->getProtocolVersion()}\r\n";
+ foreach ($this->request->getHeaders() as $name => $values) {
+ if (\strpos($name, ':') !== false) {
+ $expected = -1;
+ break;
+ }
+ foreach ($values as $value) {
+ $headers .= "$name: $value\r\n";
+ ++$expected;
+ }
+ }
+
+ /** @var array $m legacy PHP 5.3 only */
+ if (!\preg_match('#^\S+ \S+ HTTP/1\.[01]\r\n#m', $headers) || \substr_count($headers, "\n") !== ($expected + 1) || (\PHP_VERSION_ID >= 50400 ? \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers) : \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers, $m)) !== $expected) {
+ $this->closeError(new \InvalidArgumentException('Unable to send request with invalid request headers'));
+ return;
+ }
+
+ $connectionRef = &$this->connection;
+ $stateRef = &$this->state;
+ $pendingWrites = &$this->pendingWrites;
+ $that = $this;
+
+ $promise = $this->connectionManager->connect($this->request->getUri());
+ $promise->then(
+ function (ConnectionInterface $connection) use ($headers, &$connectionRef, &$stateRef, &$pendingWrites, $that) {
+ $connectionRef = $connection;
+ assert($connectionRef instanceof ConnectionInterface);
+
+ $connection->on('drain', array($that, 'handleDrain'));
+ $connection->on('data', array($that, 'handleData'));
+ $connection->on('end', array($that, 'handleEnd'));
+ $connection->on('error', array($that, 'handleError'));
+ $connection->on('close', array($that, 'close'));
+
+ $more = $connection->write($headers . "\r\n" . $pendingWrites);
+
+ assert($stateRef === ClientRequestStream::STATE_WRITING_HEAD);
+ $stateRef = ClientRequestStream::STATE_HEAD_WRITTEN;
+
+ // clear pending writes if non-empty
+ if ($pendingWrites !== '') {
+ $pendingWrites = '';
+
+ if ($more) {
+ $that->emit('drain');
+ }
+ }
+ },
+ array($this, 'closeError')
+ );
+
+ $this->on('close', function() use ($promise) {
+ $promise->cancel();
+ });
+ }
+
+ public function write($data)
+ {
+ if (!$this->isWritable()) {
+ return false;
+ }
+
+ // write directly to connection stream if already available
+ if (self::STATE_HEAD_WRITTEN <= $this->state) {
+ return $this->connection->write($data);
+ }
+
+ // otherwise buffer and try to establish connection
+ $this->pendingWrites .= $data;
+ if (self::STATE_WRITING_HEAD > $this->state) {
+ $this->writeHead();
+ }
+
+ return false;
+ }
+
+ public function end($data = null)
+ {
+ if (!$this->isWritable()) {
+ return;
+ }
+
+ if (null !== $data) {
+ $this->write($data);
+ } else if (self::STATE_WRITING_HEAD > $this->state) {
+ $this->writeHead();
+ }
+
+ $this->ended = true;
+ }
+
+ /** @internal */
+ public function handleDrain()
+ {
+ $this->emit('drain');
+ }
+
+ /** @internal */
+ public function handleData($data)
+ {
+ $this->buffer .= $data;
+
+ // buffer until double CRLF (or double LF for compatibility with legacy servers)
+ $eom = \strpos($this->buffer, "\r\n\r\n");
+ $eomLegacy = \strpos($this->buffer, "\n\n");
+ if ($eom !== false || $eomLegacy !== false) {
+ try {
+ if ($eom !== false && ($eomLegacy === false || $eom < $eomLegacy)) {
+ $response = Response::parseMessage(\substr($this->buffer, 0, $eom + 2));
+ $bodyChunk = (string) \substr($this->buffer, $eom + 4);
+ } else {
+ $response = Response::parseMessage(\substr($this->buffer, 0, $eomLegacy + 1));
+ $bodyChunk = (string) \substr($this->buffer, $eomLegacy + 2);
+ }
+ } catch (\InvalidArgumentException $exception) {
+ $this->closeError($exception);
+ return;
+ }
+
+ // response headers successfully received => remove listeners for connection events
+ $connection = $this->connection;
+ assert($connection instanceof ConnectionInterface);
+ $connection->removeListener('drain', array($this, 'handleDrain'));
+ $connection->removeListener('data', array($this, 'handleData'));
+ $connection->removeListener('end', array($this, 'handleEnd'));
+ $connection->removeListener('error', array($this, 'handleError'));
+ $connection->removeListener('close', array($this, 'close'));
+ $this->connection = null;
+ $this->buffer = '';
+
+ // take control over connection handling and check if we can reuse the connection once response body closes
+ $that = $this;
+ $request = $this->request;
+ $connectionManager = $this->connectionManager;
+ $successfulEndReceived = false;
+ $input = $body = new CloseProtectionStream($connection);
+ $input->on('close', function () use ($connection, $that, $connectionManager, $request, $response, &$successfulEndReceived) {
+ // only reuse connection after successful response and both request and response allow keep alive
+ if ($successfulEndReceived && $connection->isReadable() && $that->hasMessageKeepAliveEnabled($response) && $that->hasMessageKeepAliveEnabled($request)) {
+ $connectionManager->keepAlive($request->getUri(), $connection);
+ } else {
+ $connection->close();
+ }
+
+ $that->close();
+ });
+
+ // determine length of response body
+ $length = null;
+ $code = $response->getStatusCode();
+ if ($this->request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code == Response::STATUS_NO_CONTENT || $code == Response::STATUS_NOT_MODIFIED) {
+ $length = 0;
+ } elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') {
+ $body = new ChunkedDecoder($body);
+ } elseif ($response->hasHeader('Content-Length')) {
+ $length = (int) $response->getHeaderLine('Content-Length');
+ }
+ $response = $response->withBody($body = new ReadableBodyStream($body, $length));
+ $body->on('end', function () use (&$successfulEndReceived) {
+ $successfulEndReceived = true;
+ });
+
+ // emit response with streaming response body (see `Sender`)
+ $this->emit('response', array($response, $body));
+
+ // re-emit HTTP response body to trigger body parsing if parts of it are buffered
+ if ($bodyChunk !== '') {
+ $input->handleData($bodyChunk);
+ } elseif ($length === 0) {
+ $input->handleEnd();
+ }
+ }
+ }
+
+ /** @internal */
+ public function handleEnd()
+ {
+ $this->closeError(new \RuntimeException(
+ "Connection ended before receiving response"
+ ));
+ }
+
+ /** @internal */
+ public function handleError(\Exception $error)
+ {
+ $this->closeError(new \RuntimeException(
+ "An error occurred in the underlying stream",
+ 0,
+ $error
+ ));
+ }
+
+ /** @internal */
+ public function closeError(\Exception $error)
+ {
+ if (self::STATE_END <= $this->state) {
+ return;
+ }
+ $this->emit('error', array($error));
+ $this->close();
+ }
+
+ public function close()
+ {
+ if (self::STATE_END <= $this->state) {
+ return;
+ }
+
+ $this->state = self::STATE_END;
+ $this->pendingWrites = '';
+ $this->buffer = '';
+
+ if ($this->connection instanceof ConnectionInterface) {
+ $this->connection->close();
+ $this->connection = null;
+ }
+
+ $this->emit('close');
+ $this->removeAllListeners();
+ }
+
+ /**
+ * @internal
+ * @return bool
+ * @link https://www.rfc-editor.org/rfc/rfc9112#section-9.3
+ * @link https://www.rfc-editor.org/rfc/rfc7230#section-6.1
+ */
+ public function hasMessageKeepAliveEnabled(MessageInterface $message)
+ {
+ // @link https://www.rfc-editor.org/rfc/rfc9110#section-7.6.1
+ $connectionOptions = \array_map('trim', \explode(',', \strtolower($message->getHeaderLine('Connection'))));
+
+ if (\in_array('close', $connectionOptions, true)) {
+ return false;
+ }
+
+ if ($message->getProtocolVersion() === '1.1') {
+ return true;
+ }
+
+ if (\in_array('keep-alive', $connectionOptions, true)) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/src/Io/Clock.php b/src/Io/Clock.php
new file mode 100644
index 00000000..92c1cb09
--- /dev/null
+++ b/src/Io/Clock.php
@@ -0,0 +1,54 @@
+loop = $loop;
+ }
+
+ /** @return float */
+ public function now()
+ {
+ if ($this->now === null) {
+ $this->now = \microtime(true);
+
+ // remember clock for current loop tick only and update on next tick
+ $now =& $this->now;
+ $this->loop->futureTick(function () use (&$now) {
+ assert($now !== null);
+ $now = null;
+ });
+ }
+
+ return $this->now;
+ }
+}
diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php
index 536694fd..539107ae 100644
--- a/src/Io/MultipartParser.php
+++ b/src/Io/MultipartParser.php
@@ -26,6 +26,13 @@ final class MultipartParser
*/
private $maxFileSize;
+ /**
+ * Based on $maxInputVars and $maxFileUploads
+ *
+ * @var int
+ */
+ private $maxMultipartBodyParts;
+
/**
* ini setting "max_input_vars"
*
@@ -62,9 +69,11 @@ final class MultipartParser
*/
private $maxFileUploads;
+ private $multipartBodyPartCount = 0;
private $postCount = 0;
private $filesCount = 0;
private $emptyCount = 0;
+ private $cursor = 0;
/**
* @param int|string|null $uploadMaxFilesize
@@ -87,6 +96,8 @@ public function __construct($uploadMaxFilesize = null, $maxFileUploads = null)
$this->uploadMaxFilesize = IniUtil::iniSizeToBytes($uploadMaxFilesize);
$this->maxFileUploads = $maxFileUploads === null ? (\ini_get('file_uploads') === '' ? 0 : (int)\ini_get('max_file_uploads')) : (int)$maxFileUploads;
+
+ $this->maxMultipartBodyParts = $this->maxInputVars + $this->maxFileUploads;
}
public function parse(ServerRequestInterface $request)
@@ -101,6 +112,8 @@ public function parse(ServerRequestInterface $request)
$request = $this->request;
$this->request = null;
+ $this->multipartBodyPartCount = 0;
+ $this->cursor = 0;
$this->postCount = 0;
$this->filesCount = 0;
$this->emptyCount = 0;
@@ -114,20 +127,24 @@ private function parseBody($boundary, $buffer)
$len = \strlen($boundary);
// ignore everything before initial boundary (SHOULD be empty)
- $start = \strpos($buffer, $boundary . "\r\n");
+ $this->cursor = \strpos($buffer, $boundary . "\r\n");
- while ($start !== false) {
+ while ($this->cursor !== false) {
// search following boundary (preceded by newline)
// ignore last if not followed by boundary (SHOULD end with "--")
- $start += $len + 2;
- $end = \strpos($buffer, "\r\n" . $boundary, $start);
+ $this->cursor += $len + 2;
+ $end = \strpos($buffer, "\r\n" . $boundary, $this->cursor);
if ($end === false) {
break;
}
// parse one part and continue searching for next
- $this->parsePart(\substr($buffer, $start, $end - $start));
- $start = $end;
+ $this->parsePart(\substr($buffer, $this->cursor, $end - $this->cursor));
+ $this->cursor = $end;
+
+ if (++$this->multipartBodyPartCount > $this->maxMultipartBodyParts) {
+ break;
+ }
}
}
diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php
index e5554c46..8975ce57 100644
--- a/src/Io/RequestHeaderParser.php
+++ b/src/Io/RequestHeaderParser.php
@@ -24,6 +24,17 @@ class RequestHeaderParser extends EventEmitter
{
private $maxSize = 8192;
+ /** @var Clock */
+ private $clock;
+
+ /** @var array> */
+ private $connectionParams = array();
+
+ public function __construct(Clock $clock)
+ {
+ $this->clock = $clock;
+ }
+
public function handle(ConnectionInterface $conn)
{
$buffer = '';
@@ -58,8 +69,7 @@ public function handle(ConnectionInterface $conn)
try {
$request = $that->parseRequest(
(string)\substr($buffer, 0, $endOfHeader + 2),
- $conn->getRemoteAddress(),
- $conn->getLocalAddress()
+ $conn
);
} catch (Exception $exception) {
$buffer = '';
@@ -111,171 +121,59 @@ public function handle(ConnectionInterface $conn)
/**
* @param string $headers buffer string containing request headers only
- * @param ?string $remoteSocketUri
- * @param ?string $localSocketUri
+ * @param ConnectionInterface $connection
* @return ServerRequestInterface
* @throws \InvalidArgumentException
* @internal
*/
- public function parseRequest($headers, $remoteSocketUri, $localSocketUri)
+ public function parseRequest($headers, ConnectionInterface $connection)
{
- // additional, stricter safe-guard for request line
- // because request parser doesn't properly cope with invalid ones
- $start = array();
- if (!\preg_match('#^(?[^ ]+) (?[^ ]+) HTTP/(?\d\.\d)#m', $headers, $start)) {
- throw new \InvalidArgumentException('Unable to parse invalid request-line');
- }
-
- // only support HTTP/1.1 and HTTP/1.0 requests
- if ($start['version'] !== '1.1' && $start['version'] !== '1.0') {
- throw new \InvalidArgumentException('Received request with invalid protocol version', Response::STATUS_VERSION_NOT_SUPPORTED);
- }
-
- // match all request header fields into array, thanks to @kelunik for checking the HTTP specs and coming up with this regex
- $matches = array();
- $n = \preg_match_all('/^([^()<>@,;:\\\"\/\[\]?={}\x01-\x20\x7F]++):[\x20\x09]*+((?:[\x20\x09]*+[\x21-\x7E\x80-\xFF]++)*+)[\x20\x09]*+[\r]?+\n/m', $headers, $matches, \PREG_SET_ORDER);
-
- // check number of valid header fields matches number of lines + request line
- if (\substr_count($headers, "\n") !== $n + 1) {
- throw new \InvalidArgumentException('Unable to parse invalid request header fields');
- }
-
- // format all header fields into associative array
- $host = null;
- $fields = array();
- foreach ($matches as $match) {
- $fields[$match[1]][] = $match[2];
-
- // match `Host` request header
- if ($host === null && \strtolower($match[1]) === 'host') {
- $host = $match[2];
- }
+ // reuse same connection params for all server params for this connection
+ $cid = \PHP_VERSION_ID < 70200 ? \spl_object_hash($connection) : \spl_object_id($connection);
+ if (isset($this->connectionParams[$cid])) {
+ $serverParams = $this->connectionParams[$cid];
+ } else {
+ // assign new server params for new connection
+ $serverParams = array();
+
+ // scheme is `http` unless TLS is used
+ $localSocketUri = $connection->getLocalAddress();
+ $localParts = $localSocketUri === null ? array() : \parse_url(https://github.com/$localSocketUri);
+ if (isset($localParts['scheme']) && $localParts['scheme'] === 'tls') {
+ $serverParams['HTTPS'] = 'on';
+ }
+
+ // apply SERVER_ADDR and SERVER_PORT if server address is known
+ // address should always be known, even for Unix domain sockets (UDS)
+ // but skip UDS as it doesn't have a concept of host/port.
+ if ($localSocketUri !== null && isset($localParts['host'], $localParts['port'])) {
+ $serverParams['SERVER_ADDR'] = $localParts['host'];
+ $serverParams['SERVER_PORT'] = $localParts['port'];
+ }
+
+ // apply REMOTE_ADDR and REMOTE_PORT if source address is known
+ // address should always be known, unless this is over Unix domain sockets (UDS)
+ $remoteSocketUri = $connection->getRemoteAddress();
+ if ($remoteSocketUri !== null) {
+ $remoteAddress = \parse_url(https://github.com/$remoteSocketUri);
+ $serverParams['REMOTE_ADDR'] = $remoteAddress['host'];
+ $serverParams['REMOTE_PORT'] = $remoteAddress['port'];
+ }
+
+ // remember server params for all requests from this connection, reset on connection close
+ $this->connectionParams[$cid] = $serverParams;
+ $params =& $this->connectionParams;
+ $connection->on('close', function () use (&$params, $cid) {
+ assert(\is_array($params));
+ unset($params[$cid]);
+ });
}
// create new obj implementing ServerRequestInterface by preserving all
// previous properties and restoring original request-target
- $serverParams = array(
- 'REQUEST_TIME' => \time(),
- 'REQUEST_TIME_FLOAT' => \microtime(true)
- );
-
- // scheme is `http` unless TLS is used
- $localParts = $localSocketUri === null ? array() : \parse_url(https://github.com/$localSocketUri);
- if (isset($localParts['scheme']) && $localParts['scheme'] === 'tls') {
- $scheme = 'https://';
- $serverParams['HTTPS'] = 'on';
- } else {
- $scheme = 'http://';
- }
-
- // default host if unset comes from local socket address or defaults to localhost
- $hasHost = $host !== null;
- if ($host === null) {
- $host = isset($localParts['host'], $localParts['port']) ? $localParts['host'] . ':' . $localParts['port'] : '127.0.0.1';
- }
-
- if ($start['method'] === 'OPTIONS' && $start['target'] === '*') {
- // support asterisk-form for `OPTIONS *` request line only
- $uri = $scheme . $host;
- } elseif ($start['method'] === 'CONNECT') {
- $parts = \parse_url('tcp://' . $start['target']);
-
- // check this is a valid authority-form request-target (host:port)
- if (!isset($parts['scheme'], $parts['host'], $parts['port']) || \count($parts) !== 3) {
- throw new \InvalidArgumentException('CONNECT method MUST use authority-form request target');
- }
- $uri = $scheme . $start['target'];
- } else {
- // support absolute-form or origin-form for proxy requests
- if ($start['target'][0] === '/') {
- $uri = $scheme . $host . $start['target'];
- } else {
- // ensure absolute-form request-target contains a valid URI
- $parts = \parse_url(https://github.com/$start['target']);
-
- // make sure value contains valid host component (IP or hostname), but no fragment
- if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'http' || isset($parts['fragment'])) {
- throw new \InvalidArgumentException('Invalid absolute-form request-target');
- }
-
- $uri = $start['target'];
- }
- }
-
- // apply REMOTE_ADDR and REMOTE_PORT if source address is known
- // address should always be known, unless this is over Unix domain sockets (UDS)
- if ($remoteSocketUri !== null) {
- $remoteAddress = \parse_url(https://github.com/$remoteSocketUri);
- $serverParams['REMOTE_ADDR'] = $remoteAddress['host'];
- $serverParams['REMOTE_PORT'] = $remoteAddress['port'];
- }
-
- // apply SERVER_ADDR and SERVER_PORT if server address is known
- // address should always be known, even for Unix domain sockets (UDS)
- // but skip UDS as it doesn't have a concept of host/port.
- if ($localSocketUri !== null && isset($localParts['host'], $localParts['port'])) {
- $serverParams['SERVER_ADDR'] = $localParts['host'];
- $serverParams['SERVER_PORT'] = $localParts['port'];
- }
-
- $request = new ServerRequest(
- $start['method'],
- $uri,
- $fields,
- '',
- $start['version'],
- $serverParams
- );
-
- // only assign request target if it is not in origin-form (happy path for most normal requests)
- if ($start['target'][0] !== '/') {
- $request = $request->withRequestTarget($start['target']);
- }
-
- if ($hasHost) {
- // Optional Host request header value MUST be valid (host and optional port)
- $parts = \parse_url('http://' . $request->getHeaderLine('Host'));
-
- // make sure value contains valid host component (IP or hostname)
- if (!$parts || !isset($parts['scheme'], $parts['host'])) {
- $parts = false;
- }
-
- // make sure value does not contain any other URI component
- if (\is_array($parts)) {
- unset($parts['scheme'], $parts['host'], $parts['port']);
- }
- if ($parts === false || $parts) {
- throw new \InvalidArgumentException('Invalid Host header value');
- }
- } elseif (!$hasHost && $start['version'] === '1.1' && $start['method'] !== 'CONNECT') {
- // require Host request header for HTTP/1.1 (except for CONNECT method)
- throw new \InvalidArgumentException('Missing required Host request header');
- } elseif (!$hasHost) {
- // remove default Host request header for HTTP/1.0 when not explicitly given
- $request = $request->withoutHeader('Host');
- }
-
- // ensure message boundaries are valid according to Content-Length and Transfer-Encoding request headers
- if ($request->hasHeader('Transfer-Encoding')) {
- if (\strtolower($request->getHeaderLine('Transfer-Encoding')) !== 'chunked') {
- throw new \InvalidArgumentException('Only chunked-encoding is allowed for Transfer-Encoding', Response::STATUS_NOT_IMPLEMENTED);
- }
-
- // Transfer-Encoding: chunked and Content-Length header MUST NOT be used at the same time
- // as per https://tools.ietf.org/html/rfc7230#section-3.3.3
- if ($request->hasHeader('Content-Length')) {
- throw new \InvalidArgumentException('Using both `Transfer-Encoding: chunked` and `Content-Length` is not allowed', Response::STATUS_BAD_REQUEST);
- }
- } elseif ($request->hasHeader('Content-Length')) {
- $string = $request->getHeaderLine('Content-Length');
-
- if ((string)(int)$string !== $string) {
- // Content-Length value is not an integer or not a single integer
- throw new \InvalidArgumentException('The value of `Content-Length` is not valid', Response::STATUS_BAD_REQUEST);
- }
- }
+ $serverParams['REQUEST_TIME'] = (int) ($now = $this->clock->now());
+ $serverParams['REQUEST_TIME_FLOAT'] = $now;
- return $request;
+ return ServerRequest::parseMessage($headers, $serverParams);
}
}
diff --git a/src/Io/Sender.php b/src/Io/Sender.php
index 2f04c797..5f456b2f 100644
--- a/src/Io/Sender.php
+++ b/src/Io/Sender.php
@@ -6,7 +6,6 @@
use Psr\Http\Message\ResponseInterface;
use React\EventLoop\LoopInterface;
use React\Http\Client\Client as HttpClient;
-use React\Http\Message\Response;
use React\Promise\PromiseInterface;
use React\Promise\Deferred;
use React\Socket\ConnectorInterface;
@@ -48,9 +47,9 @@ class Sender
* @param ConnectorInterface|null $connector
* @return self
*/
- public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector = null)
+ public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector)
{
- return new self(new HttpClient($loop, $connector));
+ return new self(new HttpClient(new ClientConnectionManager($connector, $loop)));
}
private $http;
@@ -74,6 +73,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();
@@ -83,7 +85,7 @@ public function send(RequestInterface $request)
} elseif ($size === 0 && \in_array($request->getMethod(), array('POST', 'PUT', 'PATCH'))) {
// only assign a "Content-Length: 0" request header if the body is expected for certain methods
$request = $request->withHeader('Content-Length', '0');
- } elseif ($body instanceof ReadableStreamInterface && $body->isReadable() && !$request->hasHeader('Content-Length')) {
+ } elseif ($body instanceof ReadableStreamInterface && $size !== 0 && $body->isReadable() && !$request->hasHeader('Content-Length')) {
// use "Transfer-Encoding: chunked" when this is a streaming body and body size is unknown
$request = $request->withHeader('Transfer-Encoding', 'chunked');
} else {
@@ -91,12 +93,12 @@ public function send(RequestInterface $request)
$size = 0;
}
- $headers = array();
- foreach ($request->getHeaders() as $name => $values) {
- $headers[$name] = implode(', ', $values);
+ // 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
@@ -108,18 +110,8 @@ public function send(RequestInterface $request)
$deferred->reject($error);
});
- $requestStream->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred, $request) {
- $length = null;
- $code = $response->getStatusCode();
- if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code == Response::STATUS_NO_CONTENT || $code == Response::STATUS_NOT_MODIFIED) {
- $length = 0;
- } elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') {
- $body = new ChunkedDecoder($body);
- } elseif ($response->hasHeader('Content-Length')) {
- $length = (int) $response->getHeaderLine('Content-Length');
- }
-
- $deferred->resolve($response->withBody(new ReadableBodyStream($body, $length)));
+ $requestStream->on('response', function (ResponseInterface $response) use ($deferred, $request) {
+ $deferred->resolve($response);
});
if ($body instanceof ReadableStreamInterface) {
diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php
index d73d527d..143edaa8 100644
--- a/src/Io/StreamingServer.php
+++ b/src/Io/StreamingServer.php
@@ -9,7 +9,6 @@
use React\Http\Message\Response;
use React\Http\Message\ServerRequest;
use React\Promise;
-use React\Promise\CancellablePromiseInterface;
use React\Promise\PromiseInterface;
use React\Socket\ConnectionInterface;
use React\Socket\ServerInterface;
@@ -84,7 +83,9 @@ final class StreamingServer extends EventEmitter
{
private $callback;
private $parser;
- private $loop;
+
+ /** @var Clock */
+ private $clock;
/**
* Creates an HTTP server that invokes the given callback for each incoming HTTP request
@@ -104,10 +105,9 @@ public function __construct(LoopInterface $loop, $requestHandler)
throw new \InvalidArgumentException('Invalid request handler given');
}
- $this->loop = $loop;
-
$this->callback = $requestHandler;
- $this->parser = new RequestHeaderParser();
+ $this->clock = new Clock($loop);
+ $this->parser = new RequestHeaderParser($this->clock);
$that = $this;
$this->parser->on('headers', function (ServerRequestInterface $request, ConnectionInterface $conn) use ($that) {
@@ -157,10 +157,17 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface
}
// cancel pending promise once connection closes
- if ($response instanceof CancellablePromiseInterface) {
- $conn->on('close', function () use ($response) {
+ $connectionOnCloseResponseCancelerHandler = function () {};
+ if ($response instanceof PromiseInterface && \method_exists($response, 'cancel')) {
+ $connectionOnCloseResponseCanceler = function () use ($response) {
$response->cancel();
- });
+ };
+ $connectionOnCloseResponseCancelerHandler = function () use ($connectionOnCloseResponseCanceler, $conn) {
+ if ($connectionOnCloseResponseCanceler !== null) {
+ $conn->removeListener('close', $connectionOnCloseResponseCanceler);
+ }
+ };
+ $conn->on('close', $connectionOnCloseResponseCanceler);
}
// happy path: response returned, handle and return immediately
@@ -201,7 +208,7 @@ function ($error) use ($that, $conn, $request) {
$that->emit('error', array($exception));
return $that->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request);
}
- );
+ )->then($connectionOnCloseResponseCancelerHandler, $connectionOnCloseResponseCancelerHandler);
}
/** @internal */
@@ -255,7 +262,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt
// assign default "Date" header from current time automatically
if (!$response->hasHeader('Date')) {
// IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT
- $response = $response->withHeader('Date', gmdate('D, d M Y H:i:s') . ' GMT');
+ $response = $response->withHeader('Date', gmdate('D, d M Y H:i:s', (int) $this->clock->now()) . ' GMT');
} elseif ($response->getHeaderLine('Date') === ''){
$response = $response->withoutHeader('Date');
}
@@ -326,13 +333,26 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt
}
// build HTTP response header by appending status line and header fields
+ $expected = 0;
$headers = "HTTP/" . $version . " " . $code . " " . $response->getReasonPhrase() . "\r\n";
foreach ($response->getHeaders() as $name => $values) {
+ if (\strpos($name, ':') !== false) {
+ $expected = -1;
+ break;
+ }
foreach ($values as $value) {
$headers .= $name . ": " . $value . "\r\n";
+ ++$expected;
}
}
+ /** @var array $m legacy PHP 5.3 only */
+ if ($code < 100 || $code > 999 || \substr_count($headers, "\n") !== ($expected + 1) || (\PHP_VERSION_ID >= 50400 ? \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers) : \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers, $m)) !== $expected) {
+ $this->emit('error', array(new \InvalidArgumentException('Unable to send response with invalid response headers')));
+ $this->writeError($connection, Response::STATUS_INTERNAL_SERVER_ERROR, $request);
+ return;
+ }
+
// response to HEAD and 1xx, 204 and 304 responses MUST NOT include a body
// exclude status 101 (Switching Protocols) here for Upgrade request handling above
if ($method === 'HEAD' || ($code >= 100 && $code < 200 && $code !== Response::STATUS_SWITCHING_PROTOCOLS) || $code === Response::STATUS_NO_CONTENT || $code === Response::STATUS_NOT_MODIFIED) {
diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php
index 7bf7008f..64738f56 100644
--- a/src/Io/Transaction.php
+++ b/src/Io/Transaction.php
@@ -5,11 +5,12 @@
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
-use RingCentral\Psr7\Request;
-use RingCentral\Psr7\Uri;
use React\EventLoop\LoopInterface;
+use React\Http\Message\Response;
use React\Http\Message\ResponseException;
+use React\Http\Message\Uri;
use React\Promise\Deferred;
+use React\Promise\Promise;
use React\Promise\PromiseInterface;
use React\Stream\ReadableStreamInterface;
@@ -67,32 +68,31 @@ public function withOptions(array $options)
public function send(RequestInterface $request)
{
- $deferred = new Deferred(function () use (&$deferred) {
- if (isset($deferred->pending)) {
- $deferred->pending->cancel();
- unset($deferred->pending);
+ $state = new ClientRequestState();
+ $deferred = new Deferred(function () use ($state) {
+ if ($state->pending !== null) {
+ $state->pending->cancel();
+ $state->pending = null;
}
});
- $deferred->numRequests = 0;
-
// use timeout from options or default to PHP's default_socket_timeout (60)
$timeout = (float)($this->timeout !== null ? $this->timeout : ini_get("default_socket_timeout"));
$loop = $this->loop;
- $this->next($request, $deferred)->then(
- function (ResponseInterface $response) use ($deferred, $loop, &$timeout) {
- if (isset($deferred->timeout)) {
- $loop->cancelTimer($deferred->timeout);
- unset($deferred->timeout);
+ $this->next($request, $deferred, $state)->then(
+ function (ResponseInterface $response) use ($state, $deferred, $loop, &$timeout) {
+ if ($state->timeout !== null) {
+ $loop->cancelTimer($state->timeout);
+ $state->timeout = null;
}
$timeout = -1;
$deferred->resolve($response);
},
- function ($e) use ($deferred, $loop, &$timeout) {
- if (isset($deferred->timeout)) {
- $loop->cancelTimer($deferred->timeout);
- unset($deferred->timeout);
+ function ($e) use ($state, $deferred, $loop, &$timeout) {
+ if ($state->timeout !== null) {
+ $loop->cancelTimer($state->timeout);
+ $state->timeout = null;
}
$timeout = -1;
$deferred->reject($e);
@@ -106,13 +106,13 @@ function ($e) use ($deferred, $loop, &$timeout) {
$body = $request->getBody();
if ($body instanceof ReadableStreamInterface && $body->isReadable()) {
$that = $this;
- $body->on('close', function () use ($that, $deferred, &$timeout) {
+ $body->on('close', function () use ($that, $deferred, $state, &$timeout) {
if ($timeout >= 0) {
- $that->applyTimeout($deferred, $timeout);
+ $that->applyTimeout($deferred, $state, $timeout);
}
});
} else {
- $this->applyTimeout($deferred, $timeout);
+ $this->applyTimeout($deferred, $state, $timeout);
}
return $deferred->promise();
@@ -120,111 +120,128 @@ function ($e) use ($deferred, $loop, &$timeout) {
/**
* @internal
- * @param Deferred $deferred
- * @param number $timeout
+ * @param number $timeout
* @return void
*/
- public function applyTimeout(Deferred $deferred, $timeout)
+ public function applyTimeout(Deferred $deferred, ClientRequestState $state, $timeout)
{
- $deferred->timeout = $this->loop->addTimer($timeout, function () use ($timeout, $deferred) {
+ $state->timeout = $this->loop->addTimer($timeout, function () use ($timeout, $deferred, $state) {
$deferred->reject(new \RuntimeException(
'Request timed out after ' . $timeout . ' seconds'
));
- if (isset($deferred->pending)) {
- $deferred->pending->cancel();
- unset($deferred->pending);
+ if ($state->pending !== null) {
+ $state->pending->cancel();
+ $state->pending = null;
}
});
}
- private function next(RequestInterface $request, Deferred $deferred)
+ private function next(RequestInterface $request, Deferred $deferred, ClientRequestState $state)
{
$this->progress('request', array($request));
$that = $this;
- ++$deferred->numRequests;
+ ++$state->numRequests;
$promise = $this->sender->send($request);
if (!$this->streaming) {
- $promise = $promise->then(function ($response) use ($deferred, $that) {
- return $that->bufferResponse($response, $deferred);
+ $promise = $promise->then(function ($response) use ($deferred, $state, $that) {
+ return $that->bufferResponse($response, $deferred, $state);
});
}
- $deferred->pending = $promise;
+ $state->pending = $promise;
return $promise->then(
- function (ResponseInterface $response) use ($request, $that, $deferred) {
- return $that->onResponse($response, $request, $deferred);
+ function (ResponseInterface $response) use ($request, $that, $deferred, $state) {
+ return $that->onResponse($response, $request, $deferred, $state);
}
);
}
/**
* @internal
- * @param ResponseInterface $response
* @return PromiseInterface Promise
*/
- public function bufferResponse(ResponseInterface $response, $deferred)
+ public function bufferResponse(ResponseInterface $response, Deferred $deferred, ClientRequestState $state)
{
- $stream = $response->getBody();
+ $body = $response->getBody();
+ $size = $body->getSize();
- $size = $stream->getSize();
if ($size !== null && $size > $this->maximumSize) {
- $stream->close();
+ $body->close();
return \React\Promise\reject(new \OverflowException(
'Response body size of ' . $size . ' bytes exceeds maximum of ' . $this->maximumSize . ' bytes',
- \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 0
+ \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90
));
}
// body is not streaming => already buffered
- if (!$stream instanceof ReadableStreamInterface) {
+ if (!$body instanceof ReadableStreamInterface) {
return \React\Promise\resolve($response);
}
- // buffer stream and resolve with buffered body
+ /** @var ?\Closure $closer */
+ $closer = null;
$maximumSize = $this->maximumSize;
- $promise = \React\Promise\Stream\buffer($stream, $maximumSize)->then(
- function ($body) use ($response) {
- return $response->withBody(new BufferedBody($body));
- },
- function ($e) use ($stream, $maximumSize) {
- // try to close stream if buffering fails (or is cancelled)
- $stream->close();
- if ($e instanceof \OverflowException) {
- $e = new \OverflowException(
+ return $state->pending = new Promise(function ($resolve, $reject) use ($body, $maximumSize, $response, &$closer) {
+ // resolve with current buffer when stream closes successfully
+ $buffer = '';
+ $body->on('close', $closer = function () use (&$buffer, $response, $maximumSize, $resolve, $reject) {
+ $resolve($response->withBody(new BufferedBody($buffer)));
+ });
+
+ // buffer response body data in memory
+ $body->on('data', function ($data) use (&$buffer, $maximumSize, $body, $closer, $reject) {
+ $buffer .= $data;
+
+ // close stream and reject promise if limit is exceeded
+ if (isset($buffer[$maximumSize])) {
+ $buffer = '';
+ assert($closer instanceof \Closure);
+ $body->removeListener('close', $closer);
+ $body->close();
+
+ $reject(new \OverflowException(
'Response body size exceeds maximum of ' . $maximumSize . ' bytes',
- \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 0
- );
+ \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90
+ ));
}
+ });
- throw $e;
- }
- );
-
- $deferred->pending = $promise;
+ // reject buffering if body emits error
+ $body->on('error', function (\Exception $e) use ($reject) {
+ $reject(new \RuntimeException(
+ 'Error while buffering response body: ' . $e->getMessage(),
+ $e->getCode(),
+ $e
+ ));
+ });
+ }, function () use ($body, &$closer) {
+ // cancelled buffering: remove close handler to avoid resolving, then close and reject
+ assert($closer instanceof \Closure);
+ $body->removeListener('close', $closer);
+ $body->close();
- return $promise;
+ throw new \RuntimeException('Cancelled buffering response body');
+ });
}
/**
* @internal
- * @param ResponseInterface $response
- * @param RequestInterface $request
* @throws ResponseException
* @return ResponseInterface|PromiseInterface
*/
- public function onResponse(ResponseInterface $response, RequestInterface $request, $deferred)
+ public function onResponse(ResponseInterface $response, RequestInterface $request, Deferred $deferred, ClientRequestState $state)
{
$this->progress('response', array($response, $request));
// follow 3xx (Redirection) response status codes if Location header is present and not explicitly disabled
// @link https://tools.ietf.org/html/rfc7231#section-6.4
if ($this->followRedirects && ($response->getStatusCode() >= 300 && $response->getStatusCode() < 400) && $response->hasHeader('Location')) {
- return $this->onResponseRedirect($response, $request, $deferred);
+ return $this->onResponseRedirect($response, $request, $deferred, $state);
}
// only status codes 200-399 are considered to be valid, reject otherwise
@@ -239,46 +256,56 @@ public function onResponse(ResponseInterface $response, RequestInterface $reques
/**
* @param ResponseInterface $response
* @param RequestInterface $request
+ * @param Deferred $deferred
+ * @param ClientRequestState $state
* @return PromiseInterface
* @throws \RuntimeException
*/
- private function onResponseRedirect(ResponseInterface $response, RequestInterface $request, Deferred $deferred)
+ private function onResponseRedirect(ResponseInterface $response, RequestInterface $request, Deferred $deferred, ClientRequestState $state)
{
// resolve location relative to last request URI
- $location = Uri::resolve($request->getUri(), $response->getHeaderLine('Location'));
+ $location = Uri::resolve($request->getUri(), new Uri($response->getHeaderLine('Location')));
- $request = $this->makeRedirectRequest($request, $location);
+ $request = $this->makeRedirectRequest($request, $location, $response->getStatusCode());
$this->progress('redirect', array($request));
- if ($deferred->numRequests >= $this->maxRedirects) {
+ if ($state->numRequests >= $this->maxRedirects) {
throw new \RuntimeException('Maximum number of redirects (' . $this->maxRedirects . ') exceeded');
}
- return $this->next($request, $deferred);
+ return $this->next($request, $deferred, $state);
}
/**
* @param RequestInterface $request
* @param UriInterface $location
+ * @param int $statusCode
* @return RequestInterface
+ * @throws \RuntimeException
*/
- private function makeRedirectRequest(RequestInterface $request, UriInterface $location)
+ private function makeRedirectRequest(RequestInterface $request, UriInterface $location, $statusCode)
{
- $originalHost = $request->getUri()->getHost();
- $request = $request
- ->withoutHeader('Host')
- ->withoutHeader('Content-Type')
- ->withoutHeader('Content-Length');
-
// Remove authorization if changing hostnames (but not if just changing ports or protocols).
+ $originalHost = $request->getUri()->getHost();
if ($location->getHost() !== $originalHost) {
$request = $request->withoutHeader('Authorization');
}
- // naïve approach..
- $method = ($request->getMethod() === 'HEAD') ? 'HEAD' : 'GET';
+ $request = $request->withoutHeader('Host')->withUri($location);
+
+ if ($statusCode === Response::STATUS_TEMPORARY_REDIRECT || $statusCode === Response::STATUS_PERMANENT_REDIRECT) {
+ if ($request->getBody() instanceof ReadableStreamInterface) {
+ throw new \RuntimeException('Unable to redirect request with streaming body');
+ }
+ } else {
+ $request = $request
+ ->withMethod($request->getMethod() === 'HEAD' ? 'HEAD' : 'GET')
+ ->withoutHeader('Content-Type')
+ ->withoutHeader('Content-Length')
+ ->withBody(new BufferedBody(''));
+ }
- return new Request($method, $location, $request->getHeaders());
+ return $request;
}
private function progress($name, array $args = array())
diff --git a/src/Message/Request.php b/src/Message/Request.php
new file mode 100644
index 00000000..3de8c1b3
--- /dev/null
+++ b/src/Message/Request.php
@@ -0,0 +1,57 @@
+ Internally, this implementation builds on top of a base class which is
+ * considered an implementation detail that may change in the future.
+ *
+ * @see RequestInterface
+ */
+final class Request extends AbstractRequest implements RequestInterface
+{
+ /**
+ * @param string $method HTTP method for the request.
+ * @param string|UriInterface $url URL for the request.
+ * @param array $headers Headers for the message.
+ * @param string|ReadableStreamInterface|StreamInterface $body Message body.
+ * @param string $version HTTP protocol version.
+ * @throws \InvalidArgumentException for an invalid URL or body
+ */
+ public function __construct(
+ $method,
+ $url,
+ array $headers = array(),
+ $body = '',
+ $version = '1.1'
+ ) {
+ if (\is_string($body)) {
+ $body = new BufferedBody($body);
+ } elseif ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) {
+ $body = new ReadableBodyStream($body);
+ } elseif (!$body instanceof StreamInterface) {
+ throw new \InvalidArgumentException('Invalid request body given');
+ }
+
+ parent::__construct($method, $url, $headers, $body, $version);
+ }
+}
diff --git a/src/Message/Response.php b/src/Message/Response.php
index edd6245b..fa6366ed 100644
--- a/src/Message/Response.php
+++ b/src/Message/Response.php
@@ -3,11 +3,12 @@
namespace React\Http\Message;
use Fig\Http\Message\StatusCodeInterface;
+use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
+use React\Http\Io\AbstractMessage;
use React\Http\Io\BufferedBody;
use React\Http\Io\HttpBodyStream;
use React\Stream\ReadableStreamInterface;
-use RingCentral\Psr7\Response as Psr7Response;
/**
* Represents an outgoing server response message.
@@ -34,13 +35,12 @@
* `404 Not Found` status codes can used as `Response::STATUS_OK` and
* `Response::STATUS_NOT_FOUND` respectively.
*
- * > Internally, this implementation builds on top of an existing incoming
- * response message and only adds required streaming support. This base class is
+ * > Internally, this implementation builds on top a base class which is
* considered an implementation detail that may change in the future.
*
* @see \Psr\Http\Message\ResponseInterface
*/
-final class Response extends Psr7Response implements StatusCodeInterface
+final class Response extends AbstractMessage implements ResponseInterface, StatusCodeInterface
{
/**
* Create an HTML response
@@ -257,6 +257,41 @@ public static function xml($xml)
return new self(self::STATUS_OK, array('Content-Type' => 'application/xml'), $xml);
}
+ /**
+ * @var bool
+ * @see self::$phrasesMap
+ */
+ private static $phrasesInitialized = false;
+
+ /**
+ * Map of standard HTTP status codes to standard reason phrases.
+ *
+ * This map will be fully populated with all standard reason phrases on
+ * first access. By default, it only contains a subset of HTTP status codes
+ * that have a custom mapping to reason phrases (such as those with dashes
+ * and all caps words). See `self::STATUS_*` for all possible status code
+ * constants.
+ *
+ * @var array
+ * @see self::STATUS_*
+ * @see self::getReasonPhraseForStatusCode()
+ */
+ private static $phrasesMap = array(
+ 200 => 'OK',
+ 203 => 'Non-Authoritative Information',
+ 207 => 'Multi-Status',
+ 226 => 'IM Used',
+ 414 => 'URI Too Large',
+ 418 => 'I\'m a teapot',
+ 505 => 'HTTP Version Not Supported'
+ );
+
+ /** @var int */
+ private $statusCode;
+
+ /** @var string */
+ private $reasonPhrase;
+
/**
* @param int $status HTTP status code (e.g. 200/404), see `self::STATUS_*` constants
* @param array $headers additional response headers
@@ -280,12 +315,100 @@ public function __construct(
throw new \InvalidArgumentException('Invalid response body given');
}
- parent::__construct(
- $status,
+ parent::__construct($version, $headers, $body);
+
+ $this->statusCode = (int) $status;
+ $this->reasonPhrase = ($reason !== '' && $reason !== null) ? (string) $reason : self::getReasonPhraseForStatusCode($status);
+ }
+
+ public function getStatusCode()
+ {
+ return $this->statusCode;
+ }
+
+ public function withStatus($code, $reasonPhrase = '')
+ {
+ if ((string) $reasonPhrase === '') {
+ $reasonPhrase = self::getReasonPhraseForStatusCode($code);
+ }
+
+ if ($this->statusCode === (int) $code && $this->reasonPhrase === (string) $reasonPhrase) {
+ return $this;
+ }
+
+ $response = clone $this;
+ $response->statusCode = (int) $code;
+ $response->reasonPhrase = (string) $reasonPhrase;
+
+ return $response;
+ }
+
+ public function getReasonPhrase()
+ {
+ return $this->reasonPhrase;
+ }
+
+ /**
+ * @param int $code
+ * @return string default reason phrase for given status code or empty string if unknown
+ */
+ private static function getReasonPhraseForStatusCode($code)
+ {
+ if (!self::$phrasesInitialized) {
+ self::$phrasesInitialized = true;
+
+ // map all `self::STATUS_` constants from status code to reason phrase
+ // e.g. `self::STATUS_NOT_FOUND = 404` will be mapped to `404 Not Found`
+ $ref = new \ReflectionClass(__CLASS__);
+ foreach ($ref->getConstants() as $name => $value) {
+ if (!isset(self::$phrasesMap[$value]) && \strpos($name, 'STATUS_') === 0) {
+ self::$phrasesMap[$value] = \ucwords(\strtolower(\str_replace('_', ' ', \substr($name, 7))));
+ }
+ }
+ }
+
+ return isset(self::$phrasesMap[$code]) ? self::$phrasesMap[$code] : '';
+ }
+
+ /**
+ * [Internal] Parse incoming HTTP protocol message
+ *
+ * @internal
+ * @param string $message
+ * @return self
+ * @throws \InvalidArgumentException if given $message is not a valid HTTP response message
+ */
+ public static function parseMessage($message)
+ {
+ $start = array();
+ if (!\preg_match('#^HTTP/(?\d\.\d) (?\d{3})(?: (?[^\r\n]*+))?[\r]?+\n#m', $message, $start)) {
+ throw new \InvalidArgumentException('Unable to parse invalid status-line');
+ }
+
+ // only support HTTP/1.1 and HTTP/1.0 requests
+ if ($start['version'] !== '1.1' && $start['version'] !== '1.0') {
+ throw new \InvalidArgumentException('Received response with invalid protocol version');
+ }
+
+ // check number of valid header fields matches number of lines + status line
+ $matches = array();
+ $n = \preg_match_all(self::REGEX_HEADERS, $message, $matches, \PREG_SET_ORDER);
+ if (\substr_count($message, "\n") !== $n + 1) {
+ throw new \InvalidArgumentException('Unable to parse invalid response header fields');
+ }
+
+ // format all header fields into associative array
+ $headers = array();
+ foreach ($matches as $match) {
+ $headers[$match[1]][] = $match[2];
+ }
+
+ return new self(
+ (int) $start['status'],
$headers,
- $body,
- $version,
- $reason
+ '',
+ $start['version'],
+ isset($start['reason']) ? $start['reason'] : ''
);
}
}
diff --git a/src/Message/ServerRequest.php b/src/Message/ServerRequest.php
index f446f24e..32a0f62f 100644
--- a/src/Message/ServerRequest.php
+++ b/src/Message/ServerRequest.php
@@ -5,10 +5,10 @@
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
+use React\Http\Io\AbstractRequest;
use React\Http\Io\BufferedBody;
use React\Http\Io\HttpBodyStream;
use React\Stream\ReadableStreamInterface;
-use RingCentral\Psr7\Request;
/**
* Respresents an incoming server request message.
@@ -24,13 +24,12 @@
* Likewise, you can also use this class in test cases to test how your web
* application reacts to certain HTTP requests.
*
- * > Internally, this implementation builds on top of an existing outgoing
- * request message and only adds required server methods. This base class is
+ * > Internally, this implementation builds on top of a base class which is
* considered an implementation detail that may change in the future.
*
* @see ServerRequestInterface
*/
-final class ServerRequest extends Request implements ServerRequestInterface
+final class ServerRequest extends AbstractRequest implements ServerRequestInterface
{
private $attributes = array();
@@ -57,26 +56,22 @@ public function __construct(
$version = '1.1',
$serverParams = array()
) {
- $stream = null;
if (\is_string($body)) {
$body = new BufferedBody($body);
} elseif ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) {
- $stream = $body;
- $body = null;
+ $temp = new self($method, '', $headers);
+ $size = (int) $temp->getHeaderLine('Content-Length');
+ if (\strtolower($temp->getHeaderLine('Transfer-Encoding')) === 'chunked') {
+ $size = null;
+ }
+ $body = new HttpBodyStream($body, $size);
} elseif (!$body instanceof StreamInterface) {
throw new \InvalidArgumentException('Invalid server request body given');
}
- $this->serverParams = $serverParams;
parent::__construct($method, $url, $headers, $body, $version);
- if ($stream !== null) {
- $size = (int) $this->getHeaderLine('Content-Length');
- if (\strtolower($this->getHeaderLine('Transfer-Encoding')) === 'chunked') {
- $size = null;
- }
- $this->stream = new HttpBodyStream($stream, $size);
- }
+ $this->serverParams = $serverParams;
$query = $this->getUri()->getQuery();
if ($query !== '') {
@@ -186,7 +181,7 @@ private function parseCookie($cookie)
$nameValuePair = \explode('=', $pair, 2);
if (\count($nameValuePair) === 2) {
- $key = \urldecode($nameValuePair[0]);
+ $key = $nameValuePair[0];
$value = \urldecode($nameValuePair[1]);
$result[$key] = $value;
}
@@ -194,4 +189,143 @@ private function parseCookie($cookie)
return $result;
}
+
+ /**
+ * [Internal] Parse incoming HTTP protocol message
+ *
+ * @internal
+ * @param string $message
+ * @param array $serverParams
+ * @return self
+ * @throws \InvalidArgumentException if given $message is not a valid HTTP request message
+ */
+ public static function parseMessage($message, array $serverParams)
+ {
+ // parse request line like "GET /path HTTP/1.1"
+ $start = array();
+ if (!\preg_match('#^(?[^ ]+) (?[^ ]+) HTTP/(?\d\.\d)#m', $message, $start)) {
+ throw new \InvalidArgumentException('Unable to parse invalid request-line');
+ }
+
+ // only support HTTP/1.1 and HTTP/1.0 requests
+ if ($start['version'] !== '1.1' && $start['version'] !== '1.0') {
+ throw new \InvalidArgumentException('Received request with invalid protocol version', Response::STATUS_VERSION_NOT_SUPPORTED);
+ }
+
+ // check number of valid header fields matches number of lines + request line
+ $matches = array();
+ $n = \preg_match_all(self::REGEX_HEADERS, $message, $matches, \PREG_SET_ORDER);
+ if (\substr_count($message, "\n") !== $n + 1) {
+ throw new \InvalidArgumentException('Unable to parse invalid request header fields');
+ }
+
+ // format all header fields into associative array
+ $host = null;
+ $headers = array();
+ foreach ($matches as $match) {
+ $headers[$match[1]][] = $match[2];
+
+ // match `Host` request header
+ if ($host === null && \strtolower($match[1]) === 'host') {
+ $host = $match[2];
+ }
+ }
+
+ // scheme is `http` unless TLS is used
+ $scheme = isset($serverParams['HTTPS']) ? 'https://' : 'http://';
+
+ // default host if unset comes from local socket address or defaults to localhost
+ $hasHost = $host !== null;
+ if ($host === null) {
+ $host = isset($serverParams['SERVER_ADDR'], $serverParams['SERVER_PORT']) ? $serverParams['SERVER_ADDR'] . ':' . $serverParams['SERVER_PORT'] : '127.0.0.1';
+ }
+
+ if ($start['method'] === 'OPTIONS' && $start['target'] === '*') {
+ // support asterisk-form for `OPTIONS *` request line only
+ $uri = $scheme . $host;
+ } elseif ($start['method'] === 'CONNECT') {
+ $parts = \parse_url('tcp://' . $start['target']);
+
+ // check this is a valid authority-form request-target (host:port)
+ if (!isset($parts['scheme'], $parts['host'], $parts['port']) || \count($parts) !== 3) {
+ throw new \InvalidArgumentException('CONNECT method MUST use authority-form request target');
+ }
+ $uri = $scheme . $start['target'];
+ } else {
+ // support absolute-form or origin-form for proxy requests
+ if ($start['target'][0] === '/') {
+ $uri = $scheme . $host . $start['target'];
+ } else {
+ // ensure absolute-form request-target contains a valid URI
+ $parts = \parse_url(https://github.com/$start['target']);
+
+ // make sure value contains valid host component (IP or hostname), but no fragment
+ if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'http' || isset($parts['fragment'])) {
+ throw new \InvalidArgumentException('Invalid absolute-form request-target');
+ }
+
+ $uri = $start['target'];
+ }
+ }
+
+ $request = new self(
+ $start['method'],
+ $uri,
+ $headers,
+ '',
+ $start['version'],
+ $serverParams
+ );
+
+ // only assign request target if it is not in origin-form (happy path for most normal requests)
+ if ($start['target'][0] !== '/') {
+ $request = $request->withRequestTarget($start['target']);
+ }
+
+ if ($hasHost) {
+ // Optional Host request header value MUST be valid (host and optional port)
+ $parts = \parse_url('http://' . $request->getHeaderLine('Host'));
+
+ // make sure value contains valid host component (IP or hostname)
+ if (!$parts || !isset($parts['scheme'], $parts['host'])) {
+ $parts = false;
+ }
+
+ // make sure value does not contain any other URI component
+ if (\is_array($parts)) {
+ unset($parts['scheme'], $parts['host'], $parts['port']);
+ }
+ if ($parts === false || $parts) {
+ throw new \InvalidArgumentException('Invalid Host header value');
+ }
+ } elseif (!$hasHost && $start['version'] === '1.1' && $start['method'] !== 'CONNECT') {
+ // require Host request header for HTTP/1.1 (except for CONNECT method)
+ throw new \InvalidArgumentException('Missing required Host request header');
+ } elseif (!$hasHost) {
+ // remove default Host request header for HTTP/1.0 when not explicitly given
+ $request = $request->withoutHeader('Host');
+ }
+
+ // ensure message boundaries are valid according to Content-Length and Transfer-Encoding request headers
+ if ($request->hasHeader('Transfer-Encoding')) {
+ if (\strtolower($request->getHeaderLine('Transfer-Encoding')) !== 'chunked') {
+ throw new \InvalidArgumentException('Only chunked-encoding is allowed for Transfer-Encoding', Response::STATUS_NOT_IMPLEMENTED);
+ }
+
+ // Transfer-Encoding: chunked and Content-Length header MUST NOT be used at the same time
+ // as per https://tools.ietf.org/html/rfc7230#section-3.3.3
+ if ($request->hasHeader('Content-Length')) {
+ throw new \InvalidArgumentException('Using both `Transfer-Encoding: chunked` and `Content-Length` is not allowed', Response::STATUS_BAD_REQUEST);
+ }
+ } elseif ($request->hasHeader('Content-Length')) {
+ $string = $request->getHeaderLine('Content-Length');
+
+ if ((string)(int)$string !== $string) {
+ // Content-Length value is not an integer or not a single integer
+ throw new \InvalidArgumentException('The value of `Content-Length` is not valid', Response::STATUS_BAD_REQUEST);
+ }
+ }
+
+ return $request;
+ }
}
diff --git a/src/Message/Uri.php b/src/Message/Uri.php
new file mode 100644
index 00000000..1eaf24fe
--- /dev/null
+++ b/src/Message/Uri.php
@@ -0,0 +1,356 @@
+scheme = \strtolower($parts['scheme']);
+ }
+
+ if (isset($parts['user']) || isset($parts['pass'])) {
+ $this->userInfo = $this->encode(isset($parts['user']) ? $parts['user'] : '', \PHP_URL_USER) . (isset($parts['pass']) ? ':' . $this->encode($parts['pass'], \PHP_URL_PASS) : '');
+ }
+
+ if (isset($parts['host'])) {
+ $this->host = \strtolower($parts['host']);
+ }
+
+ if (isset($parts['port']) && !(($parts['port'] === 80 && $this->scheme === 'http') || ($parts['port'] === 443 && $this->scheme === 'https'))) {
+ $this->port = $parts['port'];
+ }
+
+ if (isset($parts['path'])) {
+ $this->path = $this->encode($parts['path'], \PHP_URL_PATH);
+ }
+
+ if (isset($parts['query'])) {
+ $this->query = $this->encode($parts['query'], \PHP_URL_QUERY);
+ }
+
+ if (isset($parts['fragment'])) {
+ $this->fragment = $this->encode($parts['fragment'], \PHP_URL_FRAGMENT);
+ }
+ }
+
+ public function getScheme()
+ {
+ return $this->scheme;
+ }
+
+ public function getAuthority()
+ {
+ if ($this->host === '') {
+ return '';
+ }
+
+ return ($this->userInfo !== '' ? $this->userInfo . '@' : '') . $this->host . ($this->port !== null ? ':' . $this->port : '');
+ }
+
+ public function getUserInfo()
+ {
+ return $this->userInfo;
+ }
+
+ public function getHost()
+ {
+ return $this->host;
+ }
+
+ public function getPort()
+ {
+ return $this->port;
+ }
+
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ public function getFragment()
+ {
+ return $this->fragment;
+ }
+
+ public function withScheme($scheme)
+ {
+ $scheme = \strtolower($scheme);
+ if ($scheme === $this->scheme) {
+ return $this;
+ }
+
+ if (!\preg_match('#^[a-z]*$#', $scheme)) {
+ throw new \InvalidArgumentException('Invalid URI scheme given');
+ }
+
+ $new = clone $this;
+ $new->scheme = $scheme;
+
+ if (($this->port === 80 && $scheme === 'http') || ($this->port === 443 && $scheme === 'https')) {
+ $new->port = null;
+ }
+
+ return $new;
+ }
+
+ public function withUserInfo($user, $password = null)
+ {
+ $userInfo = $this->encode($user, \PHP_URL_USER) . ($password !== null ? ':' . $this->encode($password, \PHP_URL_PASS) : '');
+ if ($userInfo === $this->userInfo) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->userInfo = $userInfo;
+
+ return $new;
+ }
+
+ public function withHost($host)
+ {
+ $host = \strtolower($host);
+ if ($host === $this->host) {
+ return $this;
+ }
+
+ if (\preg_match('#[\s%+]#', $host) || ($host !== '' && \parse_url('http://' . $host, \PHP_URL_HOST) !== $host)) {
+ throw new \InvalidArgumentException('Invalid URI host given');
+ }
+
+ $new = clone $this;
+ $new->host = $host;
+
+ return $new;
+ }
+
+ public function withPort($port)
+ {
+ $port = $port === null ? null : (int) $port;
+ if (($port === 80 && $this->scheme === 'http') || ($port === 443 && $this->scheme === 'https')) {
+ $port = null;
+ }
+
+ if ($port === $this->port) {
+ return $this;
+ }
+
+ if ($port !== null && ($port < 1 || $port > 0xffff)) {
+ throw new \InvalidArgumentException('Invalid URI port given');
+ }
+
+ $new = clone $this;
+ $new->port = $port;
+
+ return $new;
+ }
+
+ public function withPath($path)
+ {
+ $path = $this->encode($path, \PHP_URL_PATH);
+ if ($path === $this->path) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->path = $path;
+
+ return $new;
+ }
+
+ public function withQuery($query)
+ {
+ $query = $this->encode($query, \PHP_URL_QUERY);
+ if ($query === $this->query) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->query = $query;
+
+ return $new;
+ }
+
+ public function withFragment($fragment)
+ {
+ $fragment = $this->encode($fragment, \PHP_URL_FRAGMENT);
+ if ($fragment === $this->fragment) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->fragment = $fragment;
+
+ return $new;
+ }
+
+ public function __toString()
+ {
+ $uri = '';
+ if ($this->scheme !== '') {
+ $uri .= $this->scheme . ':';
+ }
+
+ $authority = $this->getAuthority();
+ if ($authority !== '') {
+ $uri .= '//' . $authority;
+ }
+
+ if ($authority !== '' && isset($this->path[0]) && $this->path[0] !== '/') {
+ $uri .= '/' . $this->path;
+ } elseif ($authority === '' && isset($this->path[0]) && $this->path[0] === '/') {
+ $uri .= '/' . \ltrim($this->path, '/');
+ } else {
+ $uri .= $this->path;
+ }
+
+ if ($this->query !== '') {
+ $uri .= '?' . $this->query;
+ }
+
+ if ($this->fragment !== '') {
+ $uri .= '#' . $this->fragment;
+ }
+
+ return $uri;
+ }
+
+ /**
+ * @param string $part
+ * @param int $component
+ * @return string
+ */
+ private function encode($part, $component)
+ {
+ return \preg_replace_callback(
+ '/(?:[^a-z0-9_\-\.~!\$&\'\(\)\*\+,;=' . ($component === \PHP_URL_PATH ? ':@\/' : ($component === \PHP_URL_QUERY || $component === \PHP_URL_FRAGMENT ? ':@\/\?' : '')) . '%]++|%(?![a-f0-9]{2}))/i',
+ function (array $match) {
+ return \rawurlencode($match[0]);
+ },
+ $part
+ );
+ }
+
+ /**
+ * [Internal] Resolve URI relative to base URI and return new absolute URI
+ *
+ * @internal
+ * @param UriInterface $base
+ * @param UriInterface $rel
+ * @return UriInterface
+ * @throws void
+ */
+ public static function resolve(UriInterface $base, UriInterface $rel)
+ {
+ if ($rel->getScheme() !== '') {
+ return $rel->getPath() === '' ? $rel : $rel->withPath(self::removeDotSegments($rel->getPath()));
+ }
+
+ $reset = false;
+ $new = $base;
+ if ($rel->getAuthority() !== '') {
+ $reset = true;
+ $userInfo = \explode(':', $rel->getUserInfo(), 2);
+ $new = $base->withUserInfo($userInfo[0], isset($userInfo[1]) ? $userInfo[1]: null)->withHost($rel->getHost())->withPort($rel->getPort());
+ }
+
+ if ($reset && $rel->getPath() === '') {
+ $new = $new->withPath('');
+ } elseif (($path = $rel->getPath()) !== '') {
+ $start = '';
+ if ($path === '' || $path[0] !== '/') {
+ $start = $base->getPath();
+ if (\substr($start, -1) !== '/') {
+ $start .= '/../';
+ }
+ }
+ $reset = true;
+ $new = $new->withPath(self::removeDotSegments($start . $path));
+ }
+ if ($reset || $rel->getQuery() !== '') {
+ $reset = true;
+ $new = $new->withQuery($rel->getQuery());
+ }
+ if ($reset || $rel->getFragment() !== '') {
+ $new = $new->withFragment($rel->getFragment());
+ }
+
+ return $new;
+ }
+
+ /**
+ * @param string $path
+ * @return string
+ */
+ private static function removeDotSegments($path)
+ {
+ $segments = array();
+ foreach (\explode('/', $path) as $segment) {
+ if ($segment === '..') {
+ \array_pop($segments);
+ } elseif ($segment !== '.' && $segment !== '') {
+ $segments[] = $segment;
+ }
+ }
+ return '/' . \implode('/', $segments) . ($path !== '/' && \substr($path, -1) === '/' ? '/' : '');
+ }
+}
diff --git a/src/Middleware/LimitConcurrentRequestsMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php
index 53338100..b1c00da0 100644
--- a/src/Middleware/LimitConcurrentRequestsMiddleware.php
+++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php
@@ -206,6 +206,6 @@ public function processQueue()
$first = \reset($this->queue);
unset($this->queue[key($this->queue)]);
- $first->resolve();
+ $first->resolve(null);
}
}
diff --git a/src/Middleware/RequestBodyBufferMiddleware.php b/src/Middleware/RequestBodyBufferMiddleware.php
index c13a5dec..ddb39f5e 100644
--- a/src/Middleware/RequestBodyBufferMiddleware.php
+++ b/src/Middleware/RequestBodyBufferMiddleware.php
@@ -6,7 +6,7 @@
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Io\BufferedBody;
use React\Http\Io\IniUtil;
-use React\Promise\Stream;
+use React\Promise\Promise;
use React\Stream\ReadableStreamInterface;
final class RequestBodyBufferMiddleware
@@ -29,19 +29,19 @@ public function __construct($sizeLimit = null)
$this->sizeLimit = IniUtil::iniSizeToBytes($sizeLimit);
}
- public function __invoke(ServerRequestInterface $request, $stack)
+ public function __invoke(ServerRequestInterface $request, $next)
{
$body = $request->getBody();
$size = $body->getSize();
// happy path: skip if body is known to be empty (or is already buffered)
- if ($size === 0 || !$body instanceof ReadableStreamInterface) {
+ if ($size === 0 || !$body instanceof ReadableStreamInterface || !$body->isReadable()) {
// replace with empty body if body is streaming (or buffered size exceeds limit)
if ($body instanceof ReadableStreamInterface || $size > $this->sizeLimit) {
$request = $request->withBody(new BufferedBody(''));
}
- return $stack($request);
+ return $next($request);
}
// request body of known size exceeding limit
@@ -50,21 +50,60 @@ public function __invoke(ServerRequestInterface $request, $stack)
$sizeLimit = 0;
}
- return Stream\buffer($body, $sizeLimit)->then(function ($buffer) use ($request, $stack) {
- $request = $request->withBody(new BufferedBody($buffer));
-
- return $stack($request);
- }, function ($error) use ($stack, $request, $body) {
- // On buffer overflow keep the request body stream in,
- // but ignore the contents and wait for the close event
- // before passing the request on to the next middleware.
- if ($error instanceof OverflowException) {
- return Stream\first($body, 'close')->then(function () use ($stack, $request) {
- return $stack($request);
- });
- }
+ /** @var ?\Closure $closer */
+ $closer = null;
+
+ return new Promise(function ($resolve, $reject) use ($body, &$closer, $sizeLimit, $request, $next) {
+ // buffer request body data in memory, discard but keep buffering if limit is reached
+ $buffer = '';
+ $bufferer = null;
+ $body->on('data', $bufferer = function ($data) use (&$buffer, $sizeLimit, $body, &$bufferer) {
+ $buffer .= $data;
+
+ // On buffer overflow keep the request body stream in,
+ // but ignore the contents and wait for the close event
+ // before passing the request on to the next middleware.
+ if (isset($buffer[$sizeLimit])) {
+ assert($bufferer instanceof \Closure);
+ $body->removeListener('data', $bufferer);
+ $bufferer = null;
+ $buffer = '';
+ }
+ });
+
+ // call $next with current buffer and resolve or reject with its results
+ $body->on('close', $closer = function () use (&$buffer, $request, $resolve, $reject, $next) {
+ try {
+ // resolve with result of next handler
+ $resolve($next($request->withBody(new BufferedBody($buffer))));
+ } catch (\Exception $e) {
+ $reject($e);
+ } catch (\Throwable $e) { // @codeCoverageIgnoreStart
+ // reject Errors just like Exceptions (PHP 7+)
+ $reject($e); // @codeCoverageIgnoreEnd
+ }
+ });
+
+ // reject buffering if body emits error
+ $body->on('error', function (\Exception $e) use ($reject, $body, $closer) {
+ // remove close handler to avoid resolving, then close and reject
+ assert($closer instanceof \Closure);
+ $body->removeListener('close', $closer);
+ $body->close();
+
+ $reject(new \RuntimeException(
+ 'Error while buffering request body: ' . $e->getMessage(),
+ $e->getCode(),
+ $e
+ ));
+ });
+ }, function () use ($body, &$closer) {
+ // cancelled buffering: remove close handler to avoid resolving, then close and reject
+ assert($closer instanceof \Closure);
+ $body->removeListener('close', $closer);
+ $body->close();
- throw $error;
+ throw new \RuntimeException('Cancelled buffering request body');
});
}
}
diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php
index 39be453a..fdd338d9 100644
--- a/tests/BrowserTest.php
+++ b/tests/BrowserTest.php
@@ -2,11 +2,9 @@
namespace React\Tests\Http;
-use Clue\React\Block;
use Psr\Http\Message\RequestInterface;
use React\Http\Browser;
use React\Promise\Promise;
-use RingCentral\Psr7\Uri;
class BrowserTest extends TestCase
{
@@ -61,9 +59,13 @@ public function testConstructWithConnectorAssignsGivenConnector()
$ref->setAccessible(true);
$client = $ref->getValue($sender);
- $ref = new \ReflectionProperty($client, 'connector');
+ $ref = new \ReflectionProperty($client, 'connectionManager');
$ref->setAccessible(true);
- $ret = $ref->getValue($client);
+ $connectionManager = $ref->getValue($client);
+
+ $ref = new \ReflectionProperty($connectionManager, 'connector');
+ $ref->setAccessible(true);
+ $ret = $ref->getValue($connectionManager);
$this->assertSame($connector, $ret);
}
@@ -86,9 +88,13 @@ public function testConstructWithConnectorWithLegacySignatureAssignsGivenConnect
$ref->setAccessible(true);
$client = $ref->getValue($sender);
- $ref = new \ReflectionProperty($client, 'connector');
+ $ref = new \ReflectionProperty($client, 'connectionManager');
+ $ref->setAccessible(true);
+ $connectionManager = $ref->getValue($client);
+
+ $ref = new \ReflectionProperty($connectionManager, 'connector');
$ref->setAccessible(true);
- $ret = $ref->getValue($client);
+ $ret = $ref->getValue($connectionManager);
$this->assertSame($connector, $ret);
}
@@ -503,4 +509,127 @@ public function testCancelGetRequestShouldCancelUnderlyingSocketConnection()
$promise = $this->browser->get('http://example.com/');
$promise->cancel();
}
+
+ public function testWithHeaderShouldOverwriteExistingHeader()
+ {
+ $this->browser = $this->browser->withHeader('User-Agent', 'ACMC'); //should be overwritten
+ $this->browser = $this->browser->withHeader('user-agent', 'ABC'); //should be the user-agent
+
+ $that = $this;
+ $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) {
+ $that->assertEquals(array('ABC'), $request->getHeader('UsEr-AgEnT'));
+ return true;
+ }))->willReturn(new Promise(function () { }));
+
+ $this->browser->get('http://example.com/');
+ }
+
+ public function testWithHeaderShouldBeOverwrittenByExplicitHeaderInGetMethod()
+ {
+ $this->browser = $this->browser->withHeader('User-Agent', 'ACMC');
+
+ $that = $this;
+ $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) {
+ $that->assertEquals(array('ABC'), $request->getHeader('UsEr-AgEnT'));
+ return true;
+ }))->willReturn(new Promise(function () { }));
+
+ $this->browser->get('http://example.com/', array('user-Agent' => 'ABC')); //should win
+ }
+
+ public function testWithMultipleHeadersShouldBeMergedCorrectlyWithMultipleDefaultHeaders()
+ {
+ $this->browser = $this->browser->withHeader('User-Agent', 'ACMC');
+ $this->browser = $this->browser->withHeader('User-Test', 'Test');
+ $this->browser = $this->browser->withHeader('Custom-HEADER', 'custom');
+ $this->browser = $this->browser->withHeader('just-a-header', 'header-value');
+
+ $that = $this;
+ $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) {
+ $expectedHeaders = array(
+ 'Host' => array('example.com'),
+
+ 'User-Test' => array('Test'),
+ 'just-a-header' => array('header-value'),
+
+ 'user-Agent' => array('ABC'),
+ 'another-header' => array('value'),
+ 'custom-header' => array('data'),
+ );
+
+ $that->assertEquals($expectedHeaders, $request->getHeaders());
+ return true;
+ }))->willReturn(new Promise(function () { }));
+
+ $headers = array(
+ 'user-Agent' => 'ABC', //should overwrite: 'User-Agent', 'ACMC'
+ 'another-header' => 'value',
+ 'custom-header' => 'data', //should overwrite: 'Custom-header', 'custom'
+ );
+ $this->browser->get('http://example.com/', $headers);
+ }
+
+ public function testWithoutHeaderShouldRemoveExistingHeader()
+ {
+ $this->browser = $this->browser->withHeader('User-Agent', 'ACMC');
+ $this->browser = $this->browser->withoutHeader('UsEr-AgEnT'); //should remove case-insensitive header
+
+ $that = $this;
+ $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) {
+ $that->assertEquals(array(), $request->getHeader('user-agent'));
+ return true;
+ }))->willReturn(new Promise(function () { }));
+
+ $this->browser->get('http://example.com/');
+ }
+
+ public function testWithoutHeaderConnectionShouldRemoveDefaultConnectionHeader()
+ {
+ $this->browser = $this->browser->withoutHeader('Connection');
+
+ $that = $this;
+ $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) {
+ $that->assertEquals(array(), $request->getHeader('Connection'));
+ return true;
+ }))->willReturn(new Promise(function () { }));
+
+ $this->browser->get('http://example.com/');
+ }
+
+ public function testWithHeaderConnectionShouldOverwriteDefaultConnectionHeader()
+ {
+ $this->browser = $this->browser->withHeader('Connection', 'keep-alive');
+
+ $that = $this;
+ $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) {
+ $that->assertEquals(array('keep-alive'), $request->getHeader('Connection'));
+ return true;
+ }))->willReturn(new Promise(function () { }));
+
+ $this->browser->get('http://example.com/');
+ }
+
+ public function testBrowserShouldSendDefaultUserAgentHeader()
+ {
+ $that = $this;
+ $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) {
+ $that->assertEquals(array(0 => 'ReactPHP/1'), $request->getHeader('user-agent'));
+ return true;
+ }))->willReturn(new Promise(function () { }));
+
+ $this->browser->get('http://example.com/');
+ }
+
+ public function testBrowserShouldNotSendDefaultUserAgentHeaderIfWithoutHeaderRemovesUserAgent()
+ {
+ $this->browser = $this->browser->withoutHeader('UsEr-AgEnT');
+
+ $that = $this;
+ $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) {
+ $that->assertEquals(array(), $request->getHeader('User-Agent'));
+ return true;
+ }))->willReturn(new Promise(function () { }));
+
+ $this->browser->get('http://example.com/');
+ }
}
diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php
index 64a3ea8a..5405874e 100644
--- a/tests/Client/FunctionalIntegrationTest.php
+++ b/tests/Client/FunctionalIntegrationTest.php
@@ -2,13 +2,16 @@
namespace React\Tests\Http\Client;
-use Clue\React\Block;
use Psr\Http\Message\ResponseInterface;
use React\EventLoop\Loop;
use React\Http\Client\Client;
+use React\Http\Io\ClientConnectionManager;
+use React\Http\Message\Request;
use React\Promise\Deferred;
+use React\Promise\Promise;
use React\Promise\Stream;
use React\Socket\ConnectionInterface;
+use React\Socket\Connector;
use React\Socket\SocketServer;
use React\Stream\ReadableStreamInterface;
use React\Tests\Http\TestCase;
@@ -45,122 +48,108 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection()
});
$port = parse_url(https://github.com/$socket-%3EgetAddress(), PHP_URL_PORT);
- $client = new Client(Loop::get());
- $request = $client->request('GET', 'http://localhost/' . $port);
+ $client = new Client(new ClientConnectionManager(new Connector(), Loop::get()));
+ $request = $client->request(new Request('GET', 'http://localhost/' . $port, array(), '', '1.0'));
$promise = Stream\first($request, 'close');
$request->end();
- Block\await($promise, null, self::TIMEOUT_LOCAL);
+ \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL));
}
- public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResponse()
+ public function testRequestToLocalhostWillConnectAndCloseConnectionAfterResponseWhenKeepAliveTimesOut()
{
$socket = new SocketServer('127.0.0.1:0');
- $socket->on('connection', function (ConnectionInterface $conn) use ($socket) {
- $conn->end("HTTP/1.0 200 OK\n\nbody");
- $socket->close();
- });
-
- $client = new Client(Loop::get());
- $request = $client->request('GET', str_replace('tcp:', 'http:', $socket->getAddress()));
+ $socket->on('connection', $this->expectCallableOnce());
- $once = $this->expectCallableOnceWith('body');
- $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) {
- $body->on('data', $once);
+ $promise = new Promise(function ($resolve) use ($socket) {
+ $socket->on('connection', function (ConnectionInterface $conn) use ($socket, $resolve) {
+ $conn->write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK");
+ $conn->on('close', function () use ($resolve) {
+ $resolve(null);
+ });
+ $socket->close();
+ });
});
+ $port = parse_url(https://github.com/$socket-%3EgetAddress(), PHP_URL_PORT);
+
+ $client = new Client(new ClientConnectionManager(new Connector(), Loop::get()));
+ $request = $client->request(new Request('GET', 'http://localhost/' . $port, array(), '', '1.1'));
- $promise = Stream\first($request, 'close');
$request->end();
- Block\await($promise, null, self::TIMEOUT_LOCAL);
+ \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL));
}
- /** @group internet */
- public function testSuccessfulResponseEmitsEnd()
+ public function testRequestToLocalhostWillReuseExistingConnectionForSecondRequest()
{
- // max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP
- ini_set('xdebug.max_nesting_level', 256);
+ $socket = new SocketServer('127.0.0.1:0');
+ $socket->on('connection', $this->expectCallableOnce());
- $client = new Client(Loop::get());
+ $socket->on('connection', function (ConnectionInterface $connection) use ($socket) {
+ $connection->on('data', function () use ($connection) {
+ $connection->write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK");
+ });
+ $socket->close();
+ });
+ $port = parse_url(https://github.com/$socket-%3EgetAddress(), PHP_URL_PORT);
- $request = $client->request('GET', 'http://www.google.com/');
+ $client = new Client(new ClientConnectionManager(new Connector(), Loop::get()));
- $once = $this->expectCallableOnce();
- $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) {
- $body->on('end', $once);
- });
+ $request = $client->request(new Request('GET', 'http://localhost/' . $port, array(), '', '1.1'));
+ $promise = Stream\first($request, 'close');
+ $request->end();
+ \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL));
+
+ $request = $client->request(new Request('GET', 'http://localhost/' . $port, array(), '', '1.1'));
$promise = Stream\first($request, 'close');
$request->end();
- Block\await($promise, null, self::TIMEOUT_REMOTE);
+ \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL));
}
- /** @group internet */
- public function testPostDataReturnsData()
+ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResponse()
{
- if (defined('HHVM_VERSION')) {
- $this->markTestSkipped('Not supported on HHVM');
- }
-
- // max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP
- ini_set('xdebug.max_nesting_level', 256);
-
- $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)));
-
- $deferred = new Deferred();
- $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred) {
- $deferred->resolve(Stream\buffer($body));
+ $socket = new SocketServer('127.0.0.1:0');
+ $socket->on('connection', function (ConnectionInterface $conn) use ($socket) {
+ $conn->end("HTTP/1.0 200 OK\n\nbody");
+ $socket->close();
});
- $request->on('error', 'printf');
- $request->on('error', $this->expectCallableNever());
-
- $request->end($data);
+ $client = new Client(new ClientConnectionManager(new Connector(), Loop::get()));
+ $request = $client->request(new Request('GET', str_replace('tcp:', 'http:', $socket->getAddress()), array(), '', '1.0'));
- $buffer = Block\await($deferred->promise(), null, self::TIMEOUT_REMOTE);
+ $once = $this->expectCallableOnceWith('body');
+ $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) {
+ $body->on('data', $once);
+ });
- $this->assertNotEquals('', $buffer);
+ $promise = Stream\first($request, 'close');
+ $request->end();
- $parsed = json_decode($buffer, true);
- $this->assertTrue(is_array($parsed) && isset($parsed['data']));
- $this->assertEquals(strlen($data), strlen($parsed['data']));
- $this->assertEquals($data, $parsed['data']);
+ \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL));
}
/** @group internet */
- public function testPostJsonReturnsData()
+ public function testSuccessfulResponseEmitsEnd()
{
- if (defined('HHVM_VERSION')) {
- $this->markTestSkipped('Not supported on HHVM');
- }
+ // max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP
+ ini_set('xdebug.max_nesting_level', 256);
- $client = new Client(Loop::get());
+ $client = new Client(new ClientConnectionManager(new Connector(), 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('GET', 'http://www.google.com/', array(), '', '1.0'));
- $deferred = new Deferred();
- $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred) {
- $deferred->resolve(Stream\buffer($body));
+ $once = $this->expectCallableOnce();
+ $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) {
+ $body->on('end', $once);
});
- $request->on('error', 'printf');
- $request->on('error', $this->expectCallableNever());
-
- $request->end($data);
-
- $buffer = Block\await($deferred->promise(), null, self::TIMEOUT_REMOTE);
-
- $this->assertNotEquals('', $buffer);
+ $promise = Stream\first($request, 'close');
+ $request->end();
- $parsed = json_decode($buffer, true);
- $this->assertTrue(is_array($parsed) && isset($parsed['json']));
- $this->assertEquals(json_decode($data, true), $parsed['json']);
+ \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_REMOTE));
}
/** @group internet */
@@ -169,9 +158,9 @@ public function testCancelPendingConnectionEmitsClose()
// max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP
ini_set('xdebug.max_nesting_level', 256);
- $client = new Client(Loop::get());
+ $client = new Client(new ClientConnectionManager(new Connector(), 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 7f96e152..00000000
--- a/tests/Client/RequestDataTest.php
+++ /dev/null
@@ -1,154 +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" .
- "User-Agent: ReactPHP/1\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" .
- "User-Agent: ReactPHP/1\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" .
- "User-Agent: ReactPHP/1\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" .
- "User-Agent: ReactPHP/1\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" .
- "User-Agent: ReactPHP/1\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" .
- "User-Agent: ReactPHP/1\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" .
- "User-Agent: ReactPHP/1\r\n" .
- "Authorization: Basic am9objpkdW1teQ==\r\n" .
- "\r\n";
-
- $this->assertSame($expected, $requestData->__toString());
- }
-}
diff --git a/tests/Client/RequestTest.php b/tests/Client/RequestTest.php
deleted file mode 100644
index fb2dc884..00000000
--- a/tests/Client/RequestTest.php
+++ /dev/null
@@ -1,481 +0,0 @@
-stream = $this->getMockBuilder('React\Socket\ConnectionInterface')
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->connector = $this->getMockBuilder('React\Socket\ConnectorInterface')
- ->getMock();
- }
-
- /** @test */
- public function requestShouldBindToStreamEventsAndUseconnector()
- {
- $requestData = new RequestData('GET', 'http://www.example.com/');
- $request = new Request($this->connector, $requestData);
-
- $this->successfulConnectionMock();
-
- $this->stream->expects($this->exactly(6))->method('on')->withConsecutive(
- array('drain', $this->identicalTo(array($request, 'handleDrain'))),
- array('data', $this->identicalTo(array($request, 'handleData'))),
- array('end', $this->identicalTo(array($request, 'handleEnd'))),
- array('error', $this->identicalTo(array($request, 'handleError'))),
- array('close', $this->identicalTo(array($request, 'handleClose')))
- );
-
- $this->stream->expects($this->exactly(5))->method('removeListener')->withConsecutive(
- array('drain', $this->identicalTo(array($request, 'handleDrain'))),
- array('data', $this->identicalTo(array($request, 'handleData'))),
- array('end', $this->identicalTo(array($request, 'handleEnd'))),
- array('error', $this->identicalTo(array($request, 'handleError'))),
- array('close', $this->identicalTo(array($request, 'handleClose')))
- );
-
- $request->on('end', $this->expectCallableNever());
-
- $request->end();
-
- $request->handleData("HTTP/1.0 200 OK\r\n");
- $request->handleData("Content-Type: text/plain\r\n");
- $request->handleData("\r\nbody");
- }
-
- /**
- * @test
- */
- public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme()
- {
- $requestData = new RequestData('GET', 'https://www.example.com/');
- $request = new Request($this->connector, $requestData);
-
- $this->connector->expects($this->once())->method('connect')->with('tls://www.example.com:443')->willReturn(new Promise(function () { }));
-
- $request->end();
- }
-
- /** @test */
- public function requestShouldEmitErrorIfConnectionFails()
- {
- $requestData = new RequestData('GET', 'http://www.example.com/');
- $request = new Request($this->connector, $requestData);
-
- $this->connector->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException()));
-
- $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
-
- $request->on('close', $this->expectCallableOnce());
- $request->on('end', $this->expectCallableNever());
-
- $request->end();
- }
-
- /** @test */
- public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed()
- {
- $requestData = new RequestData('GET', 'http://www.example.com/');
- $request = new Request($this->connector, $requestData);
-
- $this->successfulConnectionMock();
-
- $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
-
- $request->on('close', $this->expectCallableOnce());
- $request->on('end', $this->expectCallableNever());
-
- $request->end();
- $request->handleEnd();
- }
-
- /** @test */
- public function requestShouldEmitErrorIfConnectionEmitsError()
- {
- $requestData = new RequestData('GET', 'http://www.example.com/');
- $request = new Request($this->connector, $requestData);
-
- $this->successfulConnectionMock();
-
- $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('Exception')));
-
- $request->on('close', $this->expectCallableOnce());
- $request->on('end', $this->expectCallableNever());
-
- $request->end();
- $request->handleError(new \Exception('test'));
- }
-
- /** @test */
- public function requestShouldEmitErrorIfRequestParserThrowsException()
- {
- $requestData = new RequestData('GET', 'http://www.example.com/');
- $request = new Request($this->connector, $requestData);
-
- $this->successfulConnectionMock();
-
- $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException')));
-
- $request->end();
- $request->handleData("\r\n\r\n");
- }
-
- /**
- * @test
- */
- public function requestShouldEmitErrorIfUrlIsInvalid()
- {
- $requestData = new RequestData('GET', 'ftp://www.example.com');
- $request = new Request($this->connector, $requestData);
-
- $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException')));
-
- $this->connector->expects($this->never())
- ->method('connect');
-
- $request->end();
- }
-
- /**
- * @test
- */
- public function requestShouldEmitErrorIfUrlHasNoScheme()
- {
- $requestData = new RequestData('GET', 'www.example.com');
- $request = new Request($this->connector, $requestData);
-
- $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException')));
-
- $this->connector->expects($this->never())
- ->method('connect');
-
- $request->end();
- }
-
- /** @test */
- public function postRequestShouldSendAPostRequest()
- {
- $requestData = new RequestData('POST', 'http://www.example.com/');
- $request = new Request($this->connector, $requestData);
-
- $this->successfulConnectionMock();
-
- $this->stream
- ->expects($this->once())
- ->method('write')
- ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome post data$#"));
-
- $request->end('some post data');
-
- $request->handleData("HTTP/1.0 200 OK\r\n");
- $request->handleData("Content-Type: text/plain\r\n");
- $request->handleData("\r\nbody");
- }
-
- /** @test */
- public function writeWithAPostRequestShouldSendToTheStream()
- {
- $requestData = new RequestData('POST', 'http://www.example.com/');
- $request = new Request($this->connector, $requestData);
-
- $this->successfulConnectionMock();
-
- $this->stream->expects($this->exactly(3))->method('write')->withConsecutive(
- array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome$#")),
- array($this->identicalTo("post")),
- array($this->identicalTo("data"))
- );
-
- $request->write("some");
- $request->write("post");
- $request->end("data");
-
- $request->handleData("HTTP/1.0 200 OK\r\n");
- $request->handleData("Content-Type: text/plain\r\n");
- $request->handleData("\r\nbody");
- }
-
- /** @test */
- public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent()
- {
- $requestData = new RequestData('POST', 'http://www.example.com/');
- $request = new Request($this->connector, $requestData);
-
- $resolveConnection = $this->successfulAsyncConnectionMock();
-
- $this->stream->expects($this->exactly(2))->method('write')->withConsecutive(
- array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsomepost$#")),
- array($this->identicalTo("data"))
- )->willReturn(
- true
- );
-
- $this->assertFalse($request->write("some"));
- $this->assertFalse($request->write("post"));
-
- $request->on('drain', $this->expectCallableOnce());
- $request->once('drain', function () use ($request) {
- $request->write("data");
- $request->end();
- });
-
- $resolveConnection();
-
- $request->handleData("HTTP/1.0 200 OK\r\n");
- $request->handleData("Content-Type: text/plain\r\n");
- $request->handleData("\r\nbody");
- }
-
- /** @test */
- public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsBuffer()
- {
- $requestData = new RequestData('POST', 'http://www.example.com/');
- $request = new Request($this->connector, $requestData);
-
- $this->stream = $this->getMockBuilder('React\Socket\Connection')
- ->disableOriginalConstructor()
- ->setMethods(array('write'))
- ->getMock();
-
- $resolveConnection = $this->successfulAsyncConnectionMock();
-
- $this->stream->expects($this->exactly(2))->method('write')->withConsecutive(
- array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsomepost$#")),
- array($this->identicalTo("data"))
- )->willReturn(
- false
- );
-
- $this->assertFalse($request->write("some"));
- $this->assertFalse($request->write("post"));
-
- $request->on('drain', $this->expectCallableOnce());
- $request->once('drain', function () use ($request) {
- $request->write("data");
- $request->end();
- });
-
- $resolveConnection();
- $this->stream->emit('drain');
-
- $request->handleData("HTTP/1.0 200 OK\r\n");
- $request->handleData("Content-Type: text/plain\r\n");
- $request->handleData("\r\nbody");
- }
-
- /** @test */
- public function pipeShouldPipeDataIntoTheRequestBody()
- {
- $requestData = new RequestData('POST', 'http://www.example.com/');
- $request = new Request($this->connector, $requestData);
-
- $this->successfulConnectionMock();
-
- $this->stream->expects($this->exactly(3))->method('write')->withConsecutive(
- array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome$#")),
- array($this->identicalTo("post")),
- array($this->identicalTo("data"))
- );
-
- $loop = $this
- ->getMockBuilder('React\EventLoop\LoopInterface')
- ->getMock();
-
- $stream = fopen('php://memory', 'r+');
- $stream = new DuplexResourceStream($stream, $loop);
-
- $stream->pipe($request);
- $stream->emit('data', array('some'));
- $stream->emit('data', array('post'));
- $stream->emit('data', array('data'));
-
- $request->handleData("HTTP/1.0 200 OK\r\n");
- $request->handleData("Content-Type: text/plain\r\n");
- $request->handleData("\r\nbody");
- }
-
- /**
- * @test
- */
- public function writeShouldStartConnecting()
- {
- $requestData = new RequestData('POST', 'http://www.example.com/');
- $request = new Request($this->connector, $requestData);
-
- $this->connector->expects($this->once())
- ->method('connect')
- ->with('www.example.com:80')
- ->willReturn(new Promise(function () { }));
-
- $request->write('test');
- }
-
- /**
- * @test
- */
- public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode()
- {
- $requestData = new RequestData('POST', 'http://www.example.com/');
- $request = new Request($this->connector, $requestData);
-
- $this->connector->expects($this->once())
- ->method('connect')
- ->with('www.example.com:80')
- ->willReturn(new Promise(function () { }));
-
- $request->end();
-
- $this->assertFalse($request->isWritable());
- }
-
- /**
- * @test
- */
- public function closeShouldEmitCloseEvent()
- {
- $requestData = new RequestData('POST', 'http://www.example.com/');
- $request = new Request($this->connector, $requestData);
-
- $request->on('close', $this->expectCallableOnce());
- $request->close();
- }
-
- /**
- * @test
- */
- public function writeAfterCloseReturnsFalse()
- {
- $requestData = new RequestData('POST', 'http://www.example.com/');
- $request = new Request($this->connector, $requestData);
-
- $request->close();
-
- $this->assertFalse($request->isWritable());
- $this->assertFalse($request->write('nope'));
- }
-
- /**
- * @test
- */
- public function endAfterCloseIsNoOp()
- {
- $requestData = new RequestData('POST', 'http://www.example.com/');
- $request = new Request($this->connector, $requestData);
-
- $this->connector->expects($this->never())
- ->method('connect');
-
- $request->close();
- $request->end();
- }
-
- /**
- * @test
- */
- public function closeShouldCancelPendingConnectionAttempt()
- {
- $requestData = new RequestData('POST', 'http://www.example.com/');
- $request = new Request($this->connector, $requestData);
-
- $promise = new Promise(function () {}, function () {
- throw new \RuntimeException();
- });
-
- $this->connector->expects($this->once())
- ->method('connect')
- ->with('www.example.com:80')
- ->willReturn($promise);
-
- $request->end();
-
- $request->on('error', $this->expectCallableNever());
- $request->on('close', $this->expectCallableOnce());
-
- $request->close();
- $request->close();
- }
-
- /** @test */
- public function requestShouldRemoveAllListenerAfterClosed()
- {
- $requestData = new RequestData('GET', 'http://www.example.com/');
- $request = new Request($this->connector, $requestData);
-
- $request->on('close', function () {});
- $this->assertCount(1, $request->listeners('close'));
-
- $request->close();
- $this->assertCount(0, $request->listeners('close'));
- }
-
- private function successfulConnectionMock()
- {
- call_user_func($this->successfulAsyncConnectionMock());
- }
-
- private function successfulAsyncConnectionMock()
- {
- $deferred = new Deferred();
-
- $this->connector
- ->expects($this->once())
- ->method('connect')
- ->with('www.example.com:80')
- ->will($this->returnValue($deferred->promise()));
-
- $stream = $this->stream;
- return function () use ($deferred, $stream) {
- $deferred->resolve($stream);
- };
- }
-
- /** @test */
- public function multivalueHeader()
- {
- $requestData = new RequestData('GET', 'http://www.example.com/');
- $request = new Request($this->connector, $requestData);
-
- $this->successfulConnectionMock();
-
- $response = null;
- $request->on('response', $this->expectCallableOnce());
- $request->on('response', function ($value) use (&$response) {
- $response = $value;
- });
-
- $request->end();
-
- $request->handleData("HTTP/1.0 200 OK\r\n");
- $request->handleData("Content-Type: text/plain\r\n");
- $request->handleData("X-Xss-Protection:1; mode=block\r\n");
- $request->handleData("Cache-Control:public, must-revalidate, max-age=0\r\n");
- $request->handleData("\r\nbody");
-
- /** @var \Psr\Http\Message\ResponseInterface $response */
- $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $response);
- $this->assertEquals('1.0', $response->getProtocolVersion());
- $this->assertEquals(200, $response->getStatusCode());
- $this->assertEquals('OK', $response->getReasonPhrase());
- $this->assertEquals('text/plain', $response->getHeaderLine('Content-Type'));
- $this->assertEquals('1; mode=block', $response->getHeaderLine('X-Xss-Protection'));
- $this->assertEquals('public, must-revalidate, max-age=0', $response->getHeaderLine('Cache-Control'));
- }
-}
diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php
index 2b7bd58c..ef1b3936 100644
--- a/tests/FunctionalBrowserTest.php
+++ b/tests/FunctionalBrowserTest.php
@@ -2,28 +2,29 @@
namespace React\Tests\Http;
-use Clue\React\Block;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\EventLoop\Loop;
use React\Http\Browser;
use React\Http\HttpServer;
+use React\Http\Message\Response;
use React\Http\Message\ResponseException;
use React\Http\Middleware\StreamingRequestMiddleware;
-use React\Http\Message\Response;
use React\Promise\Promise;
use React\Promise\Stream;
use React\Socket\Connector;
use React\Socket\SocketServer;
use React\Stream\ReadableStreamInterface;
use React\Stream\ThroughStream;
-use RingCentral\Psr7\Request;
class FunctionalBrowserTest extends TestCase
{
private $browser;
private $base;
+ /** @var ?SocketServer */
+ private $socket;
+
/**
* @before
*/
@@ -88,14 +89,17 @@ public function setUpBrowserAndServer()
}
if ($path === '/delay/10') {
- return new Promise(function ($resolve) {
- Loop::addTimer(10, function () use ($resolve) {
+ $timer = null;
+ return new Promise(function ($resolve) use (&$timer) {
+ $timer = Loop::addTimer(10, function () use ($resolve) {
$resolve(new Response(
200,
array(),
'hello'
));
});
+ }, function () use (&$timer) {
+ Loop::cancelTimer($timer);
});
}
@@ -140,10 +144,20 @@ public function setUpBrowserAndServer()
var_dump($path);
});
- $socket = new SocketServer('127.0.0.1:0');
- $http->listen($socket);
- $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/';
+ $this->socket = new SocketServer('127.0.0.1:0');
+ $http->listen($this->socket);
+
+ $this->base = str_replace('tcp:', 'http:', $this->socket->getAddress()) . '/';
+ }
+
+ /**
+ * @after
+ */
+ public function cleanUpSocketServer()
+ {
+ $this->socket->close();
+ $this->socket = null;
}
/**
@@ -151,7 +165,7 @@ public function setUpBrowserAndServer()
*/
public function testSimpleRequest()
{
- Block\await($this->browser->get($this->base . 'get'));
+ \React\Async\await($this->browser->get($this->base . 'get'));
}
public function testGetRequestWithRelativeAddressRejects()
@@ -159,7 +173,7 @@ public function testGetRequestWithRelativeAddressRejects()
$promise = $this->browser->get('delay');
$this->setExpectedException('InvalidArgumentException', 'Invalid request URL given');
- Block\await($promise);
+ \React\Async\await($promise);
}
/**
@@ -167,7 +181,7 @@ public function testGetRequestWithRelativeAddressRejects()
*/
public function testGetRequestWithBaseAndRelativeAddressResolves()
{
- Block\await($this->browser->withBase($this->base)->get('get'));
+ \React\Async\await($this->browser->withBase($this->base)->get('get'));
}
/**
@@ -175,7 +189,7 @@ public function testGetRequestWithBaseAndRelativeAddressResolves()
*/
public function testGetRequestWithBaseAndFullAddressResolves()
{
- Block\await($this->browser->withBase('http://example.com/')->get($this->base . 'get'));
+ \React\Async\await($this->browser->withBase('http://example.com/')->get($this->base . 'get'));
}
public function testCancelGetRequestWillRejectRequest()
@@ -184,7 +198,7 @@ public function testCancelGetRequestWillRejectRequest()
$promise->cancel();
$this->setExpectedException('RuntimeException');
- Block\await($promise);
+ \React\Async\await($promise);
}
public function testCancelRequestWithPromiseFollowerWillRejectRequest()
@@ -195,13 +209,13 @@ public function testCancelRequestWithPromiseFollowerWillRejectRequest()
$promise->cancel();
$this->setExpectedException('RuntimeException');
- Block\await($promise);
+ \React\Async\await($promise);
}
public function testRequestWithoutAuthenticationFails()
{
$this->setExpectedException('RuntimeException');
- Block\await($this->browser->get($this->base . 'basic-auth/user/pass'));
+ \React\Async\await($this->browser->get($this->base . 'basic-auth/user/pass'));
}
/**
@@ -211,12 +225,12 @@ public function testRequestWithAuthenticationSucceeds()
{
$base = str_replace('://', '://user:pass@', $this->base);
- Block\await($this->browser->get($base . 'basic-auth/user/pass'));
+ \React\Async\await($this->browser->get($base . 'basic-auth/user/pass'));
}
/**
* ```bash
- * $ curl -vL "http://httpbin.org/redirect-to?url=http://user:pass@httpbin.org/basic-auth/user/pass"
+ * $ curl -vL "http://httpbingo.org/redirect-to?url=http://user:pass@httpbingo.org/basic-auth/user/pass"
* ```
*
* @doesNotPerformAssertions
@@ -225,12 +239,12 @@ public function testRedirectToPageWithAuthenticationSendsAuthenticationFromLocat
{
$target = str_replace('://', '://user:pass@', $this->base) . 'basic-auth/user/pass';
- Block\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($target)));
+ \React\Async\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($target)));
}
/**
* ```bash
- * $ curl -vL "http://unknown:invalid@httpbin.org/redirect-to?url=http://user:pass@httpbin.org/basic-auth/user/pass"
+ * $ curl -vL "http://unknown:invalid@httpbingo.org/redirect-to?url=http://user:pass@httpbingo.org/basic-auth/user/pass"
* ```
*
* @doesNotPerformAssertions
@@ -240,7 +254,7 @@ public function testRedirectFromPageWithInvalidAuthToPageWithCorrectAuthenticati
$base = str_replace('://', '://unknown:invalid@', $this->base);
$target = str_replace('://', '://user:pass@', $this->base) . 'basic-auth/user/pass';
- Block\await($this->browser->get($base . 'redirect-to?url=' . urlencode($target)));
+ \React\Async\await($this->browser->get($base . 'redirect-to?url=' . urlencode($target)));
}
public function testCancelRedirectedRequestShouldReject()
@@ -252,7 +266,7 @@ public function testCancelRedirectedRequestShouldReject()
});
$this->setExpectedException('RuntimeException', 'Request cancelled');
- Block\await($promise);
+ \React\Async\await($promise);
}
public function testTimeoutDelayedResponseShouldReject()
@@ -260,7 +274,7 @@ public function testTimeoutDelayedResponseShouldReject()
$promise = $this->browser->withTimeout(0.1)->get($this->base . 'delay/10');
$this->setExpectedException('RuntimeException', 'Request timed out after 0.1 seconds');
- Block\await($promise);
+ \React\Async\await($promise);
}
public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject()
@@ -270,7 +284,7 @@ public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject()
$stream->end();
$this->setExpectedException('RuntimeException', 'Request timed out after 0.1 seconds');
- Block\await($promise);
+ \React\Async\await($promise);
}
/**
@@ -278,7 +292,7 @@ public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject()
*/
public function testTimeoutFalseShouldResolveSuccessfully()
{
- Block\await($this->browser->withTimeout(false)->get($this->base . 'get'));
+ \React\Async\await($this->browser->withTimeout(false)->get($this->base . 'get'));
}
/**
@@ -286,7 +300,7 @@ public function testTimeoutFalseShouldResolveSuccessfully()
*/
public function testRedirectRequestRelative()
{
- Block\await($this->browser->get($this->base . 'redirect-to?url=get'));
+ \React\Async\await($this->browser->get($this->base . 'redirect-to?url=get'));
}
/**
@@ -294,7 +308,7 @@ public function testRedirectRequestRelative()
*/
public function testRedirectRequestAbsolute()
{
- Block\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($this->base . 'get')));
+ \React\Async\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($this->base . 'get')));
}
/**
@@ -304,7 +318,7 @@ public function testFollowingRedirectsFalseResolvesWithRedirectResult()
{
$browser = $this->browser->withFollowRedirects(false);
- Block\await($browser->get($this->base . 'redirect-to?url=get'));
+ \React\Async\await($browser->get($this->base . 'redirect-to?url=get'));
}
public function testFollowRedirectsZeroRejectsOnRedirect()
@@ -312,12 +326,12 @@ public function testFollowRedirectsZeroRejectsOnRedirect()
$browser = $this->browser->withFollowRedirects(0);
$this->setExpectedException('RuntimeException');
- Block\await($browser->get($this->base . 'redirect-to?url=get'));
+ \React\Async\await($browser->get($this->base . 'redirect-to?url=get'));
}
public function testResponseStatus204ShouldResolveWithEmptyBody()
{
- $response = Block\await($this->browser->get($this->base . 'status/204'));
+ $response = \React\Async\await($this->browser->get($this->base . 'status/204'));
$this->assertFalse($response->hasHeader('Content-Length'));
$body = $response->getBody();
@@ -327,7 +341,7 @@ public function testResponseStatus204ShouldResolveWithEmptyBody()
public function testResponseStatus304ShouldResolveWithEmptyBodyButContentLengthResponseHeader()
{
- $response = Block\await($this->browser->get($this->base . 'status/304'));
+ $response = \React\Async\await($this->browser->get($this->base . 'status/304'));
$this->assertEquals('12', $response->getHeaderLine('Content-Length'));
$body = $response->getBody();
@@ -342,7 +356,7 @@ public function testGetRequestWithResponseBufferMatchedExactlyResolves()
{
$promise = $this->browser->withResponseBuffer(5)->get($this->base . 'get');
- Block\await($promise);
+ \React\Async\await($promise);
}
public function testGetRequestWithResponseBufferExceededRejects()
@@ -352,9 +366,9 @@ public function testGetRequestWithResponseBufferExceededRejects()
$this->setExpectedException(
'OverflowException',
'Response body size of 5 bytes exceeds maximum of 4 bytes',
- defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0
+ defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90
);
- Block\await($promise);
+ \React\Async\await($promise);
}
public function testGetRequestWithResponseBufferExceededDuringStreamingRejects()
@@ -364,9 +378,9 @@ public function testGetRequestWithResponseBufferExceededDuringStreamingRejects()
$this->setExpectedException(
'OverflowException',
'Response body size exceeds maximum of 4 bytes',
- defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0
+ defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90
);
- Block\await($promise);
+ \React\Async\await($promise);
}
/**
@@ -379,7 +393,7 @@ public function testCanAccessHttps()
$this->markTestSkipped('Not supported on HHVM');
}
- Block\await($this->browser->get('https://www.google.com/'));
+ \React\Async\await($this->browser->get('https://www.google.com/'));
}
/**
@@ -400,7 +414,7 @@ public function testVerifyPeerEnabledForBadSslRejects()
$browser = new Browser($connector);
$this->setExpectedException('RuntimeException');
- Block\await($browser->get('https://self-signed.badssl.com/'));
+ \React\Async\await($browser->get('https://self-signed.badssl.com/'));
}
/**
@@ -421,7 +435,7 @@ public function testVerifyPeerDisabledForBadSslResolves()
$browser = new Browser($connector);
- Block\await($browser->get('https://self-signed.badssl.com/'));
+ \React\Async\await($browser->get('https://self-signed.badssl.com/'));
}
/**
@@ -430,13 +444,13 @@ public function testVerifyPeerDisabledForBadSslResolves()
public function testInvalidPort()
{
$this->setExpectedException('RuntimeException');
- Block\await($this->browser->get('http://www.google.com:443/'));
+ \React\Async\await($this->browser->get('http://www.google.com:443/'));
}
public function testErrorStatusCodeRejectsWithResponseException()
{
try {
- Block\await($this->browser->get($this->base . 'status/404'));
+ \React\Async\await($this->browser->get($this->base . 'status/404'));
$this->fail();
} catch (ResponseException $e) {
$this->assertEquals(404, $e->getCode());
@@ -448,14 +462,14 @@ public function testErrorStatusCodeRejectsWithResponseException()
public function testErrorStatusCodeDoesNotRejectWithRejectErrorResponseFalse()
{
- $response = Block\await($this->browser->withRejectErrorResponse(false)->get($this->base . 'status/404'));
+ $response = \React\Async\await($this->browser->withRejectErrorResponse(false)->get($this->base . 'status/404'));
$this->assertEquals(404, $response->getStatusCode());
}
public function testPostString()
{
- $response = Block\await($this->browser->post($this->base . 'post', array(), 'hello world'));
+ $response = \React\Async\await($this->browser->post($this->base . 'post', array(), 'hello world'));
$data = json_decode((string)$response->getBody(), true);
$this->assertEquals('hello world', $data['data']);
@@ -463,7 +477,7 @@ public function testPostString()
public function testRequestStreamReturnsResponseBodyUntilConnectionsEndsForHttp10()
{
- $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base . 'stream/1'));
+ $response = \React\Async\await($this->browser->withProtocolVersion('1.0')->get($this->base . 'stream/1'));
$this->assertEquals('1.0', $response->getProtocolVersion());
$this->assertFalse($response->hasHeader('Transfer-Encoding'));
@@ -474,7 +488,7 @@ public function testRequestStreamReturnsResponseBodyUntilConnectionsEndsForHttp1
public function testRequestStreamReturnsResponseWithTransferEncodingChunkedAndResponseBodyDecodedForHttp11()
{
- $response = Block\await($this->browser->get($this->base . 'stream/1'));
+ $response = \React\Async\await($this->browser->get($this->base . 'stream/1'));
$this->assertEquals('1.1', $response->getProtocolVersion());
@@ -486,7 +500,7 @@ public function testRequestStreamReturnsResponseWithTransferEncodingChunkedAndRe
public function testRequestStreamWithHeadRequestReturnsEmptyResponseBodWithTransferEncodingChunkedForHttp11()
{
- $response = Block\await($this->browser->head($this->base . 'stream/1'));
+ $response = \React\Async\await($this->browser->head($this->base . 'stream/1'));
$this->assertEquals('1.1', $response->getProtocolVersion());
@@ -505,7 +519,9 @@ public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenRes
$this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/';
- $response = Block\await($this->browser->get($this->base . 'stream/1'));
+ $response = \React\Async\await($this->browser->get($this->base . 'stream/1'));
+
+ $socket->close();
$this->assertEquals('1.1', $response->getProtocolVersion());
@@ -528,15 +544,114 @@ public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeeps
$this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/';
- $response = Block\await($this->browser->get($this->base . 'get', array()));
+ $response = \React\Async\await($this->browser->get($this->base . 'get', array()));
$this->assertEquals('hello', (string)$response->getBody());
- $ret = Block\await($closed->promise(), null, 0.1);
+ $ret = \React\Async\await(\React\Promise\Timer\timeout($closed->promise(), 0.1));
$this->assertTrue($ret);
$socket->close();
}
+ public function testRequestWithConnectionCloseHeaderWillCreateNewConnectionForSecondRequestEvenWhenServerKeepsConnectionOpen()
+ {
+ $twice = $this->expectCallableOnce();
+ $socket = new SocketServer('127.0.0.1:0');
+ $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) use ($socket, $twice) {
+ $connection->on('data', function () use ($connection) {
+ $connection->write("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello");
+ });
+
+ $socket->on('connection', $twice);
+ $socket->on('connection', function () use ($socket) {
+ $socket->close();
+ });
+ });
+
+ $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/';
+
+ // add `Connection: close` request header to disable HTTP keep-alive
+ $this->browser = $this->browser->withHeader('Connection', 'close');
+
+ $response = \React\Async\await($this->browser->get($this->base . 'get'));
+ assert($response instanceof ResponseInterface);
+ $this->assertEquals('hello', (string)$response->getBody());
+
+ $response = \React\Async\await($this->browser->get($this->base . 'get'));
+ assert($response instanceof ResponseInterface);
+ $this->assertEquals('hello', (string)$response->getBody());
+ }
+
+ public function testRequestWithHttp10WillCreateNewConnectionForSecondRequestEvenWhenServerKeepsConnectionOpen()
+ {
+ $twice = $this->expectCallableOnce();
+ $socket = new SocketServer('127.0.0.1:0');
+ $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) use ($socket, $twice) {
+ $connection->on('data', function () use ($connection) {
+ $connection->write("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello");
+ });
+
+ $socket->on('connection', $twice);
+ $socket->on('connection', function () use ($socket) {
+ $socket->close();
+ });
+ });
+
+ $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/';
+
+ // use HTTP/1.0 to disable HTTP keep-alive
+ $this->browser = $this->browser->withProtocolVersion('1.0');
+
+ $response = \React\Async\await($this->browser->get($this->base . 'get'));
+ assert($response instanceof ResponseInterface);
+ $this->assertEquals('hello', (string)$response->getBody());
+
+ $response = \React\Async\await($this->browser->get($this->base . 'get'));
+ assert($response instanceof ResponseInterface);
+ $this->assertEquals('hello', (string)$response->getBody());
+ }
+
+ public function testRequestWillReuseExistingConnectionForSecondRequestByDefault()
+ {
+ $this->socket->on('connection', $this->expectCallableOnce());
+
+ $response = \React\Async\await($this->browser->get($this->base . 'get'));
+ assert($response instanceof ResponseInterface);
+ $this->assertEquals('hello', (string)$response->getBody());
+
+ $response = \React\Async\await($this->browser->get($this->base . 'get'));
+ assert($response instanceof ResponseInterface);
+ $this->assertEquals('hello', (string)$response->getBody());
+ }
+
+ public function testRequestWithHttp10AndConnectionKeepAliveHeaderWillReuseExistingConnectionForSecondRequest()
+ {
+ $this->socket->on('connection', $this->expectCallableOnce());
+
+ $this->browser = $this->browser->withProtocolVersion('1.0');
+ $this->browser = $this->browser->withHeader('Connection', 'keep-alive');
+
+ $response = \React\Async\await($this->browser->get($this->base . 'get'));
+ assert($response instanceof ResponseInterface);
+ $this->assertEquals('hello', (string)$response->getBody());
+
+ $response = \React\Async\await($this->browser->get($this->base . 'get'));
+ assert($response instanceof ResponseInterface);
+ $this->assertEquals('hello', (string)$response->getBody());
+ }
+
+ public function testRequestWithoutConnectionHeaderWillReuseExistingConnectionForRedirectedRequest()
+ {
+ $this->socket->on('connection', $this->expectCallableOnce());
+
+ // remove default `Connection: close` request header to enable keep-alive
+ $this->browser = $this->browser->withoutHeader('Connection');
+
+ $response = \React\Async\await($this->browser->get($this->base . 'redirect-to?url=get'));
+ assert($response instanceof ResponseInterface);
+ $this->assertEquals('hello', (string)$response->getBody());
+ }
+
public function testPostStreamChunked()
{
$stream = new ThroughStream();
@@ -545,7 +660,7 @@ public function testPostStreamChunked()
$stream->end('hello world');
});
- $response = Block\await($this->browser->post($this->base . 'post', array(), $stream));
+ $response = \React\Async\await($this->browser->post($this->base . 'post', array(), $stream));
$data = json_decode((string)$response->getBody(), true);
$this->assertEquals('hello world', $data['data']);
@@ -561,7 +676,7 @@ public function testPostStreamKnownLength()
$stream->end('hello world');
});
- $response = Block\await($this->browser->post($this->base . 'post', array('Content-Length' => 11), $stream));
+ $response = \React\Async\await($this->browser->post($this->base . 'post', array('Content-Length' => 11), $stream));
$data = json_decode((string)$response->getBody(), true);
$this->assertEquals('hello world', $data['data']);
@@ -581,7 +696,7 @@ public function testPostStreamWillStartSendingRequestEvenWhenBodyDoesNotEmitData
$this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/';
$stream = new ThroughStream();
- Block\await($this->browser->post($this->base . 'post', array(), $stream));
+ \React\Async\await($this->browser->post($this->base . 'post', array(), $stream));
$socket->close();
}
@@ -591,7 +706,7 @@ public function testPostStreamClosed()
$stream = new ThroughStream();
$stream->close();
- $response = Block\await($this->browser->post($this->base . 'post', array(), $stream));
+ $response = \React\Async\await($this->browser->post($this->base . 'post', array(), $stream));
$data = json_decode((string)$response->getBody(), true);
$this->assertEquals('', $data['data']);
@@ -611,7 +726,7 @@ public function testSendsHttp11ByDefault()
$this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/';
- $response = Block\await($this->browser->get($this->base));
+ $response = \React\Async\await($this->browser->get($this->base));
$this->assertEquals('1.1', (string)$response->getBody());
$socket->close();
@@ -631,7 +746,7 @@ public function testSendsExplicitHttp10Request()
$this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/';
- $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base));
+ $response = \React\Async\await($this->browser->withProtocolVersion('1.0')->get($this->base));
$this->assertEquals('1.0', (string)$response->getBody());
$socket->close();
@@ -639,7 +754,7 @@ public function testSendsExplicitHttp10Request()
public function testHeadRequestReceivesResponseWithEmptyBodyButWithContentLengthResponseHeader()
{
- $response = Block\await($this->browser->head($this->base . 'get'));
+ $response = \React\Async\await($this->browser->head($this->base . 'get'));
$this->assertEquals('5', $response->getHeaderLine('Content-Length'));
$body = $response->getBody();
@@ -649,7 +764,7 @@ public function testHeadRequestReceivesResponseWithEmptyBodyButWithContentLength
public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndKnownSize()
{
- $response = Block\await($this->browser->requestStreaming('GET', $this->base . 'get'));
+ $response = \React\Async\await($this->browser->requestStreaming('GET', $this->base . 'get'));
$this->assertEquals('5', $response->getHeaderLine('Content-Length'));
$body = $response->getBody();
@@ -660,7 +775,7 @@ public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndKnown
public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndUnknownSizeFromStreamingEndpoint()
{
- $response = Block\await($this->browser->requestStreaming('GET', $this->base . 'stream/1'));
+ $response = \React\Async\await($this->browser->requestStreaming('GET', $this->base . 'stream/1'));
$this->assertFalse($response->hasHeader('Content-Length'));
$body = $response->getBody();
@@ -671,7 +786,7 @@ public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndUnkno
public function testRequestStreamingGetReceivesStreamingResponseBody()
{
- $buffer = Block\await(
+ $buffer = \React\Async\await(
$this->browser->requestStreaming('GET', $this->base . 'get')->then(function (ResponseInterface $response) {
return Stream\buffer($response->getBody());
})
@@ -682,7 +797,7 @@ public function testRequestStreamingGetReceivesStreamingResponseBody()
public function testRequestStreamingGetReceivesStreamingResponseBodyEvenWhenResponseBufferExceeded()
{
- $buffer = Block\await(
+ $buffer = \React\Async\await(
$this->browser->withResponseBuffer(4)->requestStreaming('GET', $this->base . 'get')->then(function (ResponseInterface $response) {
return Stream\buffer($response->getBody());
})
diff --git a/tests/FunctionalHttpServerTest.php b/tests/FunctionalHttpServerTest.php
index 6fa85903..dcd79b3e 100644
--- a/tests/FunctionalHttpServerTest.php
+++ b/tests/FunctionalHttpServerTest.php
@@ -2,7 +2,6 @@
namespace React\Tests\Http;
-use Clue\React\Block;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\EventLoop\Loop;
@@ -37,7 +36,7 @@ public function testPlainHttpOnRandomPort()
return Stream\buffer($conn);
});
- $response = Block\await($result, null, 1.0);
+ $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0));
$this->assertContainsString("HTTP/1.0 200 OK", $response);
$this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response);
@@ -64,7 +63,7 @@ function () {
return Stream\buffer($conn);
});
- $response = Block\await($result, null, 1.0);
+ $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0));
$this->assertContainsString("HTTP/1.0 404 Not Found", $response);
@@ -88,7 +87,7 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri()
return Stream\buffer($conn);
});
- $response = Block\await($result, null, 1.0);
+ $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0));
$this->assertContainsString("HTTP/1.0 200 OK", $response);
$this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response);
@@ -113,7 +112,7 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence()
return Stream\buffer($conn);
});
- $response = Block\await($result, null, 1.0);
+ $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0));
$this->assertContainsString("HTTP/1.0 200 OK", $response);
$this->assertContainsString('http://localhost:1000/', $response);
@@ -146,7 +145,7 @@ public function testSecureHttpsOnRandomPort()
return Stream\buffer($conn);
});
- $response = Block\await($result, null, 1.0);
+ $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0));
$this->assertContainsString("HTTP/1.0 200 OK", $response);
$this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response);
@@ -183,7 +182,7 @@ public function testSecureHttpsReturnsData()
return Stream\buffer($conn);
});
- $response = Block\await($result, null, 1.0);
+ $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0));
$this->assertContainsString("HTTP/1.0 200 OK", $response);
$this->assertContainsString("\r\nContent-Length: 33000\r\n", $response);
@@ -217,7 +216,7 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri()
return Stream\buffer($conn);
});
- $response = Block\await($result, null, 1.0);
+ $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0));
$this->assertContainsString("HTTP/1.0 200 OK", $response);
$this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response);
@@ -246,7 +245,7 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort()
return Stream\buffer($conn);
});
- $response = Block\await($result, null, 1.0);
+ $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0));
$this->assertContainsString("HTTP/1.0 200 OK", $response);
$this->assertContainsString('http://127.0.0.1/', $response);
@@ -275,7 +274,7 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort
return Stream\buffer($conn);
});
- $response = Block\await($result, null, 1.0);
+ $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0));
$this->assertContainsString("HTTP/1.0 200 OK", $response);
$this->assertContainsString('http://127.0.0.1/', $response);
@@ -313,7 +312,7 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort()
return Stream\buffer($conn);
});
- $response = Block\await($result, null, 1.0);
+ $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0));
$this->assertContainsString("HTTP/1.0 200 OK", $response);
$this->assertContainsString('https://127.0.0.1/', $response);
@@ -351,7 +350,7 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri()
return Stream\buffer($conn);
});
- $response = Block\await($result, null, 1.0);
+ $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0));
$this->assertContainsString("HTTP/1.0 200 OK", $response);
$this->assertContainsString('https://127.0.0.1/', $response);
@@ -380,7 +379,7 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort()
return Stream\buffer($conn);
});
- $response = Block\await($result, null, 1.0);
+ $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0));
$this->assertContainsString("HTTP/1.0 200 OK", $response);
$this->assertContainsString('http://127.0.0.1:443/', $response);
@@ -418,7 +417,7 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort()
return Stream\buffer($conn);
});
- $response = Block\await($result, null, 1.0);
+ $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0));
$this->assertContainsString("HTTP/1.0 200 OK", $response);
$this->assertContainsString('https://127.0.0.1:80/', $response);
@@ -446,7 +445,7 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody()
return Stream\buffer($conn);
});
- $response = Block\await($result, null, 1.0);
+ $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0));
$this->assertStringStartsWith("HTTP/1.0 200 OK", $response);
$this->assertStringEndsWith("\r\n\r\n", $response);
@@ -477,7 +476,7 @@ function (RequestInterface $request) use ($once) {
});
});
- Block\sleep(0.1);
+ \React\Async\await(\React\Promise\Timer\sleep(0.1));
$socket->close();
}
@@ -507,7 +506,7 @@ function (RequestInterface $request) use ($stream) {
});
// stream will be closed within 0.1s
- $ret = Block\await(Stream\first($stream, 'close'), null, 0.1);
+ $ret = \React\Async\await(\React\Promise\Timer\timeout(Stream\first($stream, 'close'), 0.1));
$socket->close();
@@ -536,7 +535,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses()
});
// await response stream to be closed
- $ret = Block\await(Stream\first($stream, 'close'), null, 1.0);
+ $ret = \React\Async\await(\React\Promise\Timer\timeout(Stream\first($stream, 'close'), 1.0));
$socket->close();
@@ -571,7 +570,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven()
return Stream\buffer($conn);
});
- $response = Block\await($result, null, 1.0);
+ $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0));
$this->assertStringStartsWith("HTTP/1.1 101 Switching Protocols\r\n", $response);
$this->assertStringEndsWith("\r\n\r\nhelloworld", $response);
@@ -608,7 +607,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven()
return Stream\buffer($conn);
});
- $response = Block\await($result, null, 1.0);
+ $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0));
$this->assertStringStartsWith("HTTP/1.1 101 Switching Protocols\r\n", $response);
$this->assertStringEndsWith("\r\n\r\nhelloworld", $response);
@@ -644,7 +643,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven()
return Stream\buffer($conn);
});
- $response = Block\await($result, null, 1.0);
+ $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0));
$this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response);
$this->assertStringEndsWith("\r\n\r\nhelloworld", $response);
@@ -684,7 +683,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive
return Stream\buffer($conn);
});
- $response = Block\await($result, null, 1.0);
+ $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0));
$this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response);
$this->assertStringEndsWith("\r\n\r\nhelloworld", $response);
@@ -717,7 +716,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData()
return Stream\buffer($conn);
});
- $response = Block\await($result, null, 1.0);
+ $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0));
$this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response);
$this->assertStringEndsWith("\r\n\r\n", $response);
@@ -727,6 +726,10 @@ public function testConnectWithClosedThroughStreamReturnsNoData()
public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing()
{
+ if (defined('HHVM_VERSION') && !interface_exists('React\Promise\PromisorInterface')) {
+ $this->markTestSkipped('Not supported on legacy HHVM with Promise v3');
+ }
+
$connector = new Connector();
$http = new HttpServer(
@@ -760,7 +763,7 @@ function (ServerRequestInterface $request) {
});
}
- $responses = Block\await(Promise\all($result), null, 1.0);
+ $responses = \React\Async\await(\React\Promise\Timer\timeout(Promise\all($result), 1.0));
foreach ($responses as $response) {
$this->assertContainsString("HTTP/1.0 200 OK", $response, $response);
diff --git a/tests/HttpServerTest.php b/tests/HttpServerTest.php
index a6d8057b..72d48468 100644
--- a/tests/HttpServerTest.php
+++ b/tests/HttpServerTest.php
@@ -2,7 +2,6 @@
namespace React\Tests\Http;
-use Clue\React\Block;
use Psr\Http\Message\ServerRequestInterface;
use React\EventLoop\Loop;
use React\Http\HttpServer;
@@ -17,6 +16,9 @@ final class HttpServerTest extends TestCase
private $connection;
private $socket;
+ /** @var ?int */
+ private $called = null;
+
/**
* @before
*/
@@ -54,9 +56,13 @@ public function testConstructWithoutLoopAssignsLoopAutomatically()
$ref->setAccessible(true);
$streamingServer = $ref->getValue($http);
- $ref = new \ReflectionProperty($streamingServer, 'loop');
+ $ref = new \ReflectionProperty($streamingServer, 'clock');
+ $ref->setAccessible(true);
+ $clock = $ref->getValue($streamingServer);
+
+ $ref = new \ReflectionProperty($clock, 'loop');
$ref->setAccessible(true);
- $loop = $ref->getValue($streamingServer);
+ $loop = $ref->getValue($clock);
$this->assertInstanceOf('React\EventLoop\LoopInterface', $loop);
}
@@ -135,7 +141,7 @@ public function testPostFormData()
$this->socket->emit('connection', array($this->connection));
$this->connection->emit('data', array("POST / HTTP/1.0\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 7\r\n\r\nfoo=bar"));
- $request = Block\await($deferred->promise());
+ $request = \React\Async\await($deferred->promise());
assert($request instanceof ServerRequestInterface);
$form = $request->getParsedBody();
@@ -173,7 +179,7 @@ public function testPostFileUpload()
}
});
- $request = Block\await($deferred->promise());
+ $request = \React\Async\await($deferred->promise());
assert($request instanceof ServerRequestInterface);
$this->assertEmpty($request->getParsedBody());
@@ -206,7 +212,7 @@ public function testPostJsonWillNotBeParsedByDefault()
$this->socket->emit('connection', array($this->connection));
$this->connection->emit('data', array("POST / HTTP/1.0\r\nContent-Type: application/json\r\nContent-Length: 6\r\n\r\n[true]"));
- $request = Block\await($deferred->promise());
+ $request = \React\Async\await($deferred->promise());
assert($request instanceof ServerRequestInterface);
$this->assertNull($request->getParsedBody());
diff --git a/tests/Io/AbstractMessageTest.php b/tests/Io/AbstractMessageTest.php
new file mode 100644
index 00000000..9e2c7d32
--- /dev/null
+++ b/tests/Io/AbstractMessageTest.php
@@ -0,0 +1,222 @@
+ $headers
+ * @param StreamInterface $body
+ */
+ public function __construct($protocolVersion, array $headers, StreamInterface $body)
+ {
+ return parent::__construct($protocolVersion, $headers, $body);
+ }
+}
+
+class AbstractMessageTest extends TestCase
+{
+ public function testWithProtocolVersionReturnsNewInstanceWhenProtocolVersionIsChanged()
+ {
+ $message = new MessageMock(
+ '1.1',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock()
+ );
+
+ $new = $message->withProtocolVersion('1.0');
+ $this->assertNotSame($message, $new);
+ $this->assertEquals('1.0', $new->getProtocolVersion());
+ $this->assertEquals('1.1', $message->getProtocolVersion());
+ }
+
+ public function testWithProtocolVersionReturnsSameInstanceWhenProtocolVersionIsUnchanged()
+ {
+ $message = new MessageMock(
+ '1.1',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock()
+ );
+
+ $new = $message->withProtocolVersion('1.1');
+ $this->assertSame($message, $new);
+ $this->assertEquals('1.1', $message->getProtocolVersion());
+ }
+
+ public function testHeaderWithStringValue()
+ {
+ $message = new MessageMock(
+ '1.1',
+ array(
+ 'Content-Type' => 'text/plain'
+ ),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock()
+ );
+
+ $this->assertEquals(array('Content-Type' => array('text/plain')), $message->getHeaders());
+
+ $this->assertEquals(array('text/plain'), $message->getHeader('Content-Type'));
+ $this->assertEquals(array('text/plain'), $message->getHeader('CONTENT-type'));
+
+ $this->assertEquals('text/plain', $message->getHeaderLine('Content-Type'));
+ $this->assertEquals('text/plain', $message->getHeaderLine('CONTENT-Type'));
+
+ $this->assertTrue($message->hasHeader('Content-Type'));
+ $this->assertTrue($message->hasHeader('content-TYPE'));
+
+ $new = $message->withHeader('Content-Type', 'text/plain');
+ $this->assertSame($message, $new);
+
+ $new = $message->withHeader('Content-Type', array('text/plain'));
+ $this->assertSame($message, $new);
+
+ $new = $message->withHeader('content-type', 'text/plain');
+ $this->assertNotSame($message, $new);
+ $this->assertEquals(array('content-type' => array('text/plain')), $new->getHeaders());
+ $this->assertEquals(array('Content-Type' => array('text/plain')), $message->getHeaders());
+
+ $new = $message->withHeader('Content-Type', 'text/html');
+ $this->assertNotSame($message, $new);
+ $this->assertEquals(array('Content-Type' => array('text/html')), $new->getHeaders());
+ $this->assertEquals(array('Content-Type' => array('text/plain')), $message->getHeaders());
+
+ $new = $message->withHeader('Content-Type', array('text/html'));
+ $this->assertNotSame($message, $new);
+ $this->assertEquals(array('Content-Type' => array('text/html')), $new->getHeaders());
+ $this->assertEquals(array('Content-Type' => array('text/plain')), $message->getHeaders());
+
+ $new = $message->withAddedHeader('Content-Type', array());
+ $this->assertSame($message, $new);
+
+ $new = $message->withoutHeader('Content-Type');
+ $this->assertNotSame($message, $new);
+ $this->assertEquals(array(), $new->getHeaders());
+ $this->assertEquals(array('Content-Type' => array('text/plain')), $message->getHeaders());
+ }
+
+ public function testHeaderWithMultipleValues()
+ {
+ $message = new MessageMock(
+ '1.1',
+ array(
+ 'Set-Cookie' => array(
+ 'a=1',
+ 'b=2'
+ )
+ ),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock()
+ );
+
+ $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2')), $message->getHeaders());
+
+ $this->assertEquals(array('a=1', 'b=2'), $message->getHeader('Set-Cookie'));
+ $this->assertEquals(array('a=1', 'b=2'), $message->getHeader('Set-Cookie'));
+
+ $this->assertEquals('a=1, b=2', $message->getHeaderLine('Set-Cookie'));
+ $this->assertEquals('a=1, b=2', $message->getHeaderLine('Set-Cookie'));
+
+ $this->assertTrue($message->hasHeader('Set-Cookie'));
+ $this->assertTrue($message->hasHeader('Set-Cookie'));
+
+ $new = $message->withHeader('Set-Cookie', array('a=1', 'b=2'));
+ $this->assertSame($message, $new);
+
+ $new = $message->withHeader('Set-Cookie', array('a=1', 'b=2', 'c=3'));
+ $this->assertNotSame($message, $new);
+ $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2', 'c=3')), $new->getHeaders());
+ $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2')), $message->getHeaders());
+
+ $new = $message->withAddedHeader('Set-Cookie', array());
+ $this->assertSame($message, $new);
+
+ $new = $message->withAddedHeader('Set-Cookie', 'c=3');
+ $this->assertNotSame($message, $new);
+ $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2', 'c=3')), $new->getHeaders());
+ $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2')), $message->getHeaders());
+
+ $new = $message->withAddedHeader('Set-Cookie', array('c=3'));
+ $this->assertNotSame($message, $new);
+ $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2', 'c=3')), $new->getHeaders());
+ $this->assertEquals(array('Set-Cookie' => array('a=1', 'b=2')), $message->getHeaders());
+ }
+
+ public function testHeaderWithEmptyValue()
+ {
+ $message = new MessageMock(
+ '1.1',
+ array(
+ 'Content-Type' => array()
+ ),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock()
+ );
+
+ $this->assertEquals(array(), $message->getHeaders());
+
+ $this->assertEquals(array(), $message->getHeader('Content-Type'));
+ $this->assertEquals('', $message->getHeaderLine('Content-Type'));
+ $this->assertFalse($message->hasHeader('Content-Type'));
+
+ $new = $message->withHeader('Empty', array());
+ $this->assertSame($message, $new);
+ $this->assertFalse($new->hasHeader('Empty'));
+
+ $new = $message->withAddedHeader('Empty', array());
+ $this->assertSame($message, $new);
+ $this->assertFalse($new->hasHeader('Empty'));
+
+ $new = $message->withoutHeader('Empty');
+ $this->assertSame($message, $new);
+ $this->assertFalse($new->hasHeader('Empty'));
+ }
+
+ public function testHeaderWithMultipleValuesAcrossMixedCaseNamesInConstructorMergesAllValuesWithNameFromLastNonEmptyValue()
+ {
+ $message = new MessageMock(
+ '1.1',
+ array(
+ 'SET-Cookie' => 'a=1',
+ 'set-cookie' => array('b=2'),
+ 'set-COOKIE' => array()
+ ),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock()
+ );
+
+ $this->assertEquals(array('set-cookie' => array('a=1', 'b=2')), $message->getHeaders());
+ $this->assertEquals(array('a=1', 'b=2'), $message->getHeader('Set-Cookie'));
+ }
+
+ public function testWithBodyReturnsNewInstanceWhenBodyIsChanged()
+ {
+ $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock();
+ $message = new MessageMock(
+ '1.1',
+ array(),
+ $body
+ );
+
+ $body2 = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock();
+ $new = $message->withBody($body2);
+ $this->assertNotSame($message, $new);
+ $this->assertSame($body2, $new->getBody());
+ $this->assertSame($body, $message->getBody());
+ }
+
+ public function testWithBodyReturnsSameInstanceWhenBodyIsUnchanged()
+ {
+ $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock();
+ $message = new MessageMock(
+ '1.1',
+ array(),
+ $body
+ );
+
+ $new = $message->withBody($body);
+ $this->assertSame($message, $new);
+ $this->assertEquals($body, $message->getBody());
+ }
+}
diff --git a/tests/Io/AbstractRequestTest.php b/tests/Io/AbstractRequestTest.php
new file mode 100644
index 00000000..7ff4a9a5
--- /dev/null
+++ b/tests/Io/AbstractRequestTest.php
@@ -0,0 +1,449 @@
+ $headers
+ * @param StreamInterface $body
+ * @param string $protocolVersion
+ */
+ public function __construct(
+ $method,
+ $uri,
+ array $headers,
+ StreamInterface $body,
+ $protocolVersion
+ ) {
+ parent::__construct($method, $uri, $headers, $body, $protocolVersion);
+ }
+}
+
+class AbstractRequestTest extends TestCase
+{
+ public function testCtorWithInvalidUriThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ new RequestMock(
+ 'GET',
+ null,
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+ }
+
+ public function testGetHeadersReturnsHostHeaderFromUri()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com/',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $this->assertEquals(array('Host' => array('example.com')), $request->getHeaders());
+ }
+
+ public function testGetHeadersReturnsHostHeaderFromUriWithCustomHttpPort()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com:8080/',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $this->assertEquals(array('Host' => array('example.com:8080')), $request->getHeaders());
+ }
+
+ public function testGetHeadersReturnsHostHeaderFromUriWithCustomPortHttpOnHttpsPort()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com:443/',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $this->assertEquals(array('Host' => array('example.com:443')), $request->getHeaders());
+ }
+
+ public function testGetHeadersReturnsHostHeaderFromUriWithCustomPortHttpsOnHttpPort()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'https://example.com:80/',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $this->assertEquals(array('Host' => array('example.com:80')), $request->getHeaders());
+ }
+
+ public function testGetHeadersReturnsHostHeaderFromUriWithoutDefaultHttpPort()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com/',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $this->assertEquals(array('Host' => array('example.com')), $request->getHeaders());
+ }
+
+ public function testGetHeadersReturnsHostHeaderFromUriWithoutDefaultHttpsPort()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'https://example.com/',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $this->assertEquals(array('Host' => array('example.com')), $request->getHeaders());
+ }
+
+ public function testGetHeadersReturnsHostHeaderFromUriBeforeOtherHeadersExplicitlyGiven()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com/',
+ array(
+ 'User-Agent' => 'demo'
+ ),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $this->assertEquals(array('Host' => array('example.com'), 'User-Agent' => array('demo')), $request->getHeaders());
+ }
+
+ public function testGetHeadersReturnsHostHeaderFromHeadersExplicitlyGiven()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://localhost/',
+ array(
+ 'Host' => 'example.com:8080'
+ ),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $this->assertEquals(array('Host' => array('example.com:8080')), $request->getHeaders());
+ }
+
+ public function testGetHeadersReturnsHostHeaderFromUriWhenHeadersExplicitlyGivenContainEmptyHostArray()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'https://example.com/',
+ array(
+ 'Host' => array()
+ ),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $this->assertEquals(array('Host' => array('example.com')), $request->getHeaders());
+ }
+
+ public function testGetRequestTargetReturnsPathAndQueryFromUri()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com/demo?name=Alice',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $this->assertEquals('/demo?name=Alice', $request->getRequestTarget());
+ }
+
+ public function testGetRequestTargetReturnsSlashOnlyIfUriHasNoPathOrQuery()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com/',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $this->assertEquals('/', $request->getRequestTarget());
+ }
+
+ public function testGetRequestTargetReturnsRequestTargetInAbsoluteFormIfGivenExplicitly()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com/demo?name=Alice',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+ $request = $request->withRequestTarget('http://example.com/demo?name=Alice');
+
+ $this->assertEquals('http://example.com/demo?name=Alice', $request->getRequestTarget());
+ }
+
+ public function testWithRequestTargetReturnsNewInstanceWhenRequestTargetIsChanged()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com/',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $new = $request->withRequestTarget('http://example.com/');
+ $this->assertNotSame($request, $new);
+ $this->assertEquals('http://example.com/', $new->getRequestTarget());
+ $this->assertEquals('/', $request->getRequestTarget());
+ }
+
+ public function testWithRequestTargetReturnsSameInstanceWhenRequestTargetIsUnchanged()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com/',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+ $request = $request->withRequestTarget('/');
+
+ $new = $request->withRequestTarget('/');
+ $this->assertSame($request, $new);
+ $this->assertEquals('/', $request->getRequestTarget());
+ }
+
+ public function testWithMethodReturnsNewInstanceWhenMethodIsChanged()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com/',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $new = $request->withMethod('POST');
+ $this->assertNotSame($request, $new);
+ $this->assertEquals('POST', $new->getMethod());
+ $this->assertEquals('GET', $request->getMethod());
+ }
+
+ public function testWithMethodReturnsSameInstanceWhenMethodIsUnchanged()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com/',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $new = $request->withMethod('GET');
+ $this->assertSame($request, $new);
+ $this->assertEquals('GET', $request->getMethod());
+ }
+
+ public function testGetUriReturnsUriInstanceGivenToCtor()
+ {
+ $uri = $this->getMockBuilder('Psr\Http\Message\UriInterface')->getMock();
+
+ $request = new RequestMock(
+ 'GET',
+ $uri,
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $this->assertSame($uri, $request->getUri());
+ }
+
+ public function testGetUriReturnsUriInstanceForUriStringGivenToCtor()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com/',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $uri = $request->getUri();
+ $this->assertInstanceOf('Psr\Http\Message\UriInterface', $uri);
+ $this->assertEquals('http://example.com/', (string) $uri);
+ }
+
+ public function testWithUriReturnsNewInstanceWhenUriIsChanged()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com/',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $uri = $this->getMockBuilder('Psr\Http\Message\UriInterface')->getMock();
+ $new = $request->withUri($uri);
+
+ $this->assertNotSame($request, $new);
+ $this->assertEquals($uri, $new->getUri());
+ $this->assertEquals('http://example.com/', (string) $request->getUri());
+ }
+
+ public function testWithUriReturnsSameInstanceWhenUriIsUnchanged()
+ {
+ $uri = $this->getMockBuilder('Psr\Http\Message\UriInterface')->getMock();
+
+ $request = new RequestMock(
+ 'GET',
+ $uri,
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $new = $request->withUri($uri);
+ $this->assertSame($request, $new);
+ $this->assertEquals($uri, $request->getUri());
+ }
+
+ public function testWithUriReturnsNewInstanceWithHostHeaderChangedIfUriContainsHost()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com/',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $uri = new Uri('http://localhost/');
+ $new = $request->withUri($uri);
+
+ $this->assertNotSame($request, $new);
+ $this->assertEquals('http://localhost/', (string) $new->getUri());
+ $this->assertEquals(array('Host' => array('localhost')), $new->getHeaders());
+ }
+
+ public function testWithUriReturnsNewInstanceWithHostHeaderChangedIfUriContainsHostWithCustomPort()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com/',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $uri = new Uri('http://localhost:8080/');
+ $new = $request->withUri($uri);
+
+ $this->assertNotSame($request, $new);
+ $this->assertEquals('http://localhost:8080/', (string) $new->getUri());
+ $this->assertEquals(array('Host' => array('localhost:8080')), $new->getHeaders());
+ }
+
+ public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderBeforeOthersIfUriContainsHost()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com/',
+ array(
+ 'User-Agent' => 'test'
+ ),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+ $request = $request->withoutHeader('Host');
+
+ $uri = new Uri('http://localhost/');
+ $new = $request->withUri($uri);
+
+ $this->assertNotSame($request, $new);
+ $this->assertEquals('http://localhost/', (string) $new->getUri());
+ $this->assertEquals(array('Host' => array('localhost'), 'User-Agent' => array('test')), $new->getHeaders());
+ }
+
+ public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfUriContainsNoHost()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com/',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $uri = new Uri('/path');
+ $new = $request->withUri($uri);
+
+ $this->assertNotSame($request, $new);
+ $this->assertEquals('/path', (string) $new->getUri());
+ $this->assertEquals(array('Host' => array('example.com')), $new->getHeaders());
+ }
+
+ public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfPreserveHostIsTrue()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com/',
+ array(),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+
+ $uri = new Uri('http://localhost/');
+ $new = $request->withUri($uri, true);
+
+ $this->assertNotSame($request, $new);
+ $this->assertEquals('http://localhost/', (string) $new->getUri());
+ $this->assertEquals(array('Host' => array('example.com')), $new->getHeaders());
+ }
+
+ public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderNoMatterIfPreserveHostIsTrue()
+ {
+ $request = new RequestMock(
+ 'GET',
+ 'http://example.com/',
+ array(
+ 'User-Agent' => 'test'
+ ),
+ $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(),
+ '1.1'
+ );
+ $request = $request->withoutHeader('Host');
+
+ $uri = new Uri('http://example.com/');
+ $new = $request->withUri($uri, true);
+
+ $this->assertNotSame($request, $new);
+ $this->assertEquals('http://example.com/', (string) $new->getUri());
+ $this->assertEquals(array('Host' => array('example.com'), 'User-Agent' => array('test')), $new->getHeaders());
+ }
+}
diff --git a/tests/Io/ClientConnectionManagerTest.php b/tests/Io/ClientConnectionManagerTest.php
new file mode 100644
index 00000000..6aafa6db
--- /dev/null
+++ b/tests/Io/ClientConnectionManagerTest.php
@@ -0,0 +1,389 @@
+getMockBuilder('React\Socket\ConnectorInterface')->getMock();
+ $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn($promise);
+
+ $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
+
+ $connectionManager = new ClientConnectionManager($connector, $loop);
+
+ $ret = $connectionManager->connect(new Uri('https://reactphp.org/'));
+
+ assert($ret instanceof PromiseInterface);
+ $this->assertSame($promise, $ret);
+ }
+
+ public function testConnectWithHttpUriShouldConnectToTcpWithDefaultPort()
+ {
+ $promise = new Promise(function () { });
+ $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
+ $connector->expects($this->once())->method('connect')->with('reactphp.org:80')->willReturn($promise);
+
+ $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
+
+ $connectionManager = new ClientConnectionManager($connector, $loop);
+
+ $ret = $connectionManager->connect(new Uri('http://reactphp.org/'));
+ $this->assertSame($promise, $ret);
+ }
+
+ public function testConnectWithExplicitPortShouldConnectWithGivenPort()
+ {
+ $promise = new Promise(function () { });
+ $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
+ $connector->expects($this->once())->method('connect')->with('reactphp.org:8080')->willReturn($promise);
+
+ $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
+
+ $connectionManager = new ClientConnectionManager($connector, $loop);
+
+ $ret = $connectionManager->connect(new Uri('http://reactphp.org:8080/'));
+ $this->assertSame($promise, $ret);
+ }
+
+ public function testConnectWithInvalidSchemeShouldRejectWithException()
+ {
+ $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
+ $connector->expects($this->never())->method('connect');
+
+ $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
+
+ $connectionManager = new ClientConnectionManager($connector, $loop);
+
+ $promise = $connectionManager->connect(new Uri('ftp://reactphp.org/'));
+
+ $exception = null;
+ $promise->then(null, function ($reason) use (&$exception) {
+ $exception = $reason;
+ });
+
+ $this->assertInstanceOf('InvalidArgumentException', $exception);
+ $this->assertEquals('Invalid request URL given', $exception->getMessage());
+ }
+
+ public function testConnectWithoutSchemeShouldRejectWithException()
+ {
+ $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
+ $connector->expects($this->never())->method('connect');
+
+ $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
+
+ $connectionManager = new ClientConnectionManager($connector, $loop);
+
+ $promise = $connectionManager->connect(new Uri('reactphp.org'));
+
+ $exception = null;
+ $promise->then(null, function ($reason) use (&$exception) {
+ $exception = $reason;
+ });
+
+ $this->assertInstanceOf('InvalidArgumentException', $exception);
+ $this->assertEquals('Invalid request URL given', $exception->getMessage());
+ }
+
+ public function testConnectReusesIdleConnectionFromPreviousKeepAliveCallWithoutUsingConnectorAndWillAddAndRemoveStreamEventsAndAddAndCancelIdleTimer()
+ {
+ $connectionToReuse = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+
+ $streamHandler = null;
+ $connectionToReuse->expects($this->exactly(3))->method('on')->withConsecutive(
+ array(
+ 'close',
+ $this->callback(function ($cb) use (&$streamHandler) {
+ $streamHandler = $cb;
+ return true;
+ })
+ ),
+ array(
+ 'data',
+ $this->callback(function ($cb) use (&$streamHandler) {
+ assert($streamHandler instanceof \Closure);
+ return $cb === $streamHandler;
+ })
+ ),
+ array(
+ 'error',
+ $this->callback(function ($cb) use (&$streamHandler) {
+ assert($streamHandler instanceof \Closure);
+ return $cb === $streamHandler;
+ })
+ )
+ );
+
+ $connectionToReuse->expects($this->exactly(3))->method('removeListener')->withConsecutive(
+ array(
+ 'close',
+ $this->callback(function ($cb) use (&$streamHandler) {
+ assert($streamHandler instanceof \Closure);
+ return $cb === $streamHandler;
+ })
+ ),
+ array(
+ 'data',
+ $this->callback(function ($cb) use (&$streamHandler) {
+ assert($streamHandler instanceof \Closure);
+ return $cb === $streamHandler;
+ })
+ ),
+ array(
+ 'error',
+ $this->callback(function ($cb) use (&$streamHandler) {
+ assert($streamHandler instanceof \Closure);
+ return $cb === $streamHandler;
+ })
+ )
+ );
+
+ $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
+ $connector->expects($this->never())->method('connect');
+
+ $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
+ $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
+ $loop->expects($this->once())->method('addTimer')->willReturn($timer);
+ $loop->expects($this->once())->method('cancelTimer')->with($timer);
+
+ $connectionManager = new ClientConnectionManager($connector, $loop);
+
+ $connectionManager->keepAlive(new Uri('https://reactphp.org/'), $connectionToReuse);
+
+ $promise = $connectionManager->connect(new Uri('https://reactphp.org/'));
+ assert($promise instanceof PromiseInterface);
+
+ $connection = null;
+ $promise->then(function ($value) use (&$connection) {
+ $connection = $value;
+ });
+
+ $this->assertSame($connectionToReuse, $connection);
+ }
+
+ public function testConnectReusesIdleConnectionFromPreviousKeepAliveCallWithoutUsingConnectorAlsoWhenUriPathAndQueryAndFragmentIsDifferent()
+ {
+ $connectionToReuse = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+
+ $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
+ $connector->expects($this->never())->method('connect');
+
+ $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
+ $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
+ $loop->expects($this->once())->method('addTimer')->willReturn($timer);
+ $loop->expects($this->once())->method('cancelTimer')->with($timer);
+
+ $connectionManager = new ClientConnectionManager($connector, $loop);
+
+ $connectionManager->keepAlive(new Uri('https://reactphp.org/http?foo#bar'), $connectionToReuse);
+
+ $promise = $connectionManager->connect(new Uri('https://reactphp.org/http/'));
+ assert($promise instanceof PromiseInterface);
+
+ $connection = null;
+ $promise->then(function ($value) use (&$connection) {
+ $connection = $value;
+ });
+
+ $this->assertSame($connectionToReuse, $connection);
+ }
+
+ public function testConnectUsesConnectorWithSameUriAndReturnsPromiseForNewConnectionFromConnectorWhenPreviousKeepAliveCallUsedDifferentUri()
+ {
+ $connectionToReuse = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+
+ $promise = new Promise(function () { });
+ $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
+ $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn($promise);
+
+ $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
+
+ $connectionManager = new ClientConnectionManager($connector, $loop);
+
+ $connectionManager->keepAlive(new Uri('http://reactphp.org/'), $connectionToReuse);
+
+ $ret = $connectionManager->connect(new Uri('https://reactphp.org/'));
+
+ assert($ret instanceof PromiseInterface);
+ $this->assertSame($promise, $ret);
+ }
+
+ public function testConnectUsesConnectorForNewConnectionWhenPreviousConnectReusedIdleConnectionFromPreviousKeepAliveCall()
+ {
+ $firstConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $secondConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+
+ $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
+ $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(\React\Promise\resolve($secondConnection));
+
+ $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
+ $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
+ $loop->expects($this->once())->method('addTimer')->willReturn($timer);
+ $loop->expects($this->once())->method('cancelTimer')->with($timer);
+
+ $connectionManager = new ClientConnectionManager($connector, $loop);
+
+ $connectionManager->keepAlive(new Uri('https://reactphp.org/'), $firstConnection);
+
+ $promise = $connectionManager->connect(new Uri('https://reactphp.org/'));
+ assert($promise instanceof PromiseInterface);
+
+ $promise = $connectionManager->connect(new Uri('https://reactphp.org/'));
+ assert($promise instanceof PromiseInterface);
+
+ $connection = null;
+ $promise->then(function ($value) use (&$connection) {
+ $connection = $value;
+ });
+
+ $this->assertSame($secondConnection, $connection);
+ }
+
+ public function testKeepAliveAddsTimerAndDoesNotCloseConnectionImmediately()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->never())->method('close');
+
+ $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
+
+ $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
+ $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything());
+
+ $connectionManager = new ClientConnectionManager($connector, $loop);
+
+ $connectionManager->keepAlive(new Uri('https://reactphp.org/'), $connection);
+ }
+
+ public function testKeepAliveClosesConnectionAfterIdleTimeout()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('close');
+
+ $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
+
+ $timerCallback = null;
+ $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
+ $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
+ $loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timerCallback) {
+ $timerCallback = $cb;
+ return true;
+ }))->willReturn($timer);
+ $loop->expects($this->once())->method('cancelTimer')->with($timer);
+
+ $connectionManager = new ClientConnectionManager($connector, $loop);
+
+ $connectionManager->keepAlive(new Uri('https://reactphp.org/'), $connection);
+
+ // manually invoker timer function to emulate time has passed
+ $this->assertNotNull($timerCallback);
+ call_user_func($timerCallback); // $timerCallback() (PHP 5.4+)
+ }
+
+ public function testConnectUsesConnectorForNewConnectionWhenIdleConnectionFromPreviousKeepAliveCallHasAlreadyTimedOut()
+ {
+ $firstConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $firstConnection->expects($this->once())->method('close');
+
+ $secondConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $secondConnection->expects($this->never())->method('close');
+
+ $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
+ $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(\React\Promise\resolve($secondConnection));
+
+ $timerCallback = null;
+ $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
+ $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
+ $loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timerCallback) {
+ $timerCallback = $cb;
+ return true;
+ }))->willReturn($timer);
+ $loop->expects($this->once())->method('cancelTimer')->with($timer);
+
+ $connectionManager = new ClientConnectionManager($connector, $loop);
+
+ $connectionManager->keepAlive(new Uri('https://reactphp.org/'), $firstConnection);
+
+ // manually invoker timer function to emulate time has passed
+ $this->assertNotNull($timerCallback);
+ call_user_func($timerCallback); // $timerCallback() (PHP 5.4+)
+
+ $promise = $connectionManager->connect(new Uri('https://reactphp.org/'));
+ assert($promise instanceof PromiseInterface);
+
+ $connection = null;
+ $promise->then(function ($value) use (&$connection) {
+ $connection = $value;
+ });
+
+ $this->assertSame($secondConnection, $connection);
+ }
+
+ public function testConnectUsesConnectorForNewConnectionWhenIdleConnectionFromPreviousKeepAliveCallHasAlreadyFiredUnexpectedStreamEventBeforeIdleTimeoutThatClosesConnection()
+ {
+ $firstConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $firstConnection->expects($this->once())->method('close');
+
+ $streamHandler = null;
+ $firstConnection->expects($this->exactly(3))->method('on')->withConsecutive(
+ array(
+ 'close',
+ $this->callback(function ($cb) use (&$streamHandler) {
+ $streamHandler = $cb;
+ return true;
+ })
+ ),
+ array(
+ 'data',
+ $this->callback(function ($cb) use (&$streamHandler) {
+ assert($streamHandler instanceof \Closure);
+ return $cb === $streamHandler;
+ })
+ ),
+ array(
+ 'error',
+ $this->callback(function ($cb) use (&$streamHandler) {
+ assert($streamHandler instanceof \Closure);
+ return $cb === $streamHandler;
+ })
+ )
+ );
+
+ $secondConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $secondConnection->expects($this->never())->method('close');
+
+ $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
+ $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(\React\Promise\resolve($secondConnection));
+
+ $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
+ $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
+ $loop->expects($this->once())->method('addTimer')->willReturn($timer);
+ $loop->expects($this->once())->method('cancelTimer')->with($timer);
+
+ $connectionManager = new ClientConnectionManager($connector, $loop);
+
+ $connectionManager->keepAlive(new Uri('https://reactphp.org/'), $firstConnection);
+
+ // manually invoke connection close to emulate server closing idle connection before idle timeout
+ $this->assertNotNull($streamHandler);
+ call_user_func($streamHandler); // $streamHandler() (PHP 5.4+)
+
+ $promise = $connectionManager->connect(new Uri('https://reactphp.org/'));
+ assert($promise instanceof PromiseInterface);
+
+ $connection = null;
+ $promise->then(function ($value) use (&$connection) {
+ $connection = $value;
+ });
+
+ $this->assertSame($secondConnection, $connection);
+ }
+}
diff --git a/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php
new file mode 100644
index 00000000..9a5373a1
--- /dev/null
+++ b/tests/Io/ClientRequestStreamTest.php
@@ -0,0 +1,1075 @@
+getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+
+ $uri = new Uri('http://www.example.com/');
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->with($uri)->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', $uri);
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $connection->expects($this->atLeast(5))->method('on')->withConsecutive(
+ array('drain', $this->identicalTo(array($request, 'handleDrain'))),
+ array('data', $this->identicalTo(array($request, 'handleData'))),
+ array('end', $this->identicalTo(array($request, 'handleEnd'))),
+ array('error', $this->identicalTo(array($request, 'handleError'))),
+ array('close', $this->identicalTo(array($request, 'close')))
+ );
+
+ $connection->expects($this->exactly(5))->method('removeListener')->withConsecutive(
+ array('drain', $this->identicalTo(array($request, 'handleDrain'))),
+ array('data', $this->identicalTo(array($request, 'handleData'))),
+ array('end', $this->identicalTo(array($request, 'handleEnd'))),
+ array('error', $this->identicalTo(array($request, 'handleError'))),
+ array('close', $this->identicalTo(array($request, 'close')))
+ );
+
+ $request->end();
+
+ $request->handleData("HTTP/1.0 200 OK\r\n");
+ $request->handleData("Content-Type: text/plain\r\n");
+ $request->handleData("\r\nbody");
+ }
+
+ /** @test */
+ public function requestShouldEmitErrorIfConnectionFails()
+ {
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException()));
+
+ $requestData = new Request('GET', 'http://www.example.com/');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+ }
+
+ /** @test */
+ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+ $request->handleEnd();
+ }
+
+ /** @test */
+ public function requestShouldEmitErrorIfConnectionEmitsError()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('Exception')));
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+ $request->handleError(new \Exception('test'));
+ }
+
+ public static function provideInvalidRequest()
+ {
+ $request = new Request('GET' , "http://localhost/");
+
+ return array(
+ array(
+ $request->withMethod("INVA\r\nLID", '')
+ ),
+ array(
+ $request->withRequestTarget('/inva lid')
+ ),
+ array(
+ $request->withHeader('Invalid', "Yes\r\n")
+ ),
+ array(
+ $request->withHeader('Invalid', "Yes\n")
+ ),
+ array(
+ $request->withHeader('Invalid', "Yes\r")
+ ),
+ array(
+ $request->withHeader("Inva\r\nlid", 'Yes')
+ ),
+ array(
+ $request->withHeader("Inva\nlid", 'Yes')
+ ),
+ array(
+ $request->withHeader("Inva\rlid", 'Yes')
+ ),
+ array(
+ $request->withHeader('Inva Lid', 'Yes')
+ ),
+ array(
+ $request->withHeader('Inva:Lid', 'Yes')
+ ),
+ array(
+ $request->withHeader('Invalid', "Val\0ue")
+ ),
+ array(
+ $request->withHeader("Inva\0lid", 'Yes')
+ )
+ );
+ }
+
+ /**
+ * @dataProvider provideInvalidRequest
+ * @param RequestInterface $request
+ */
+ public function testStreamShouldEmitErrorBeforeCreatingConnectionWhenRequestIsInvalid(RequestInterface $request)
+ {
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->never())->method('connect');
+
+ $stream = new ClientRequestStream($connectionManager, $request);
+
+ $stream->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException')));
+ $stream->on('close', $this->expectCallableOnce());
+
+ $stream->end();
+ }
+
+ /** @test */
+ public function requestShouldEmitErrorIfRequestParserThrowsException()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException')));
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+ $request->handleData("\r\n\r\n");
+ }
+
+ /** @test */
+ public function getRequestShouldSendAGetRequest()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.0\r\nHost: www.example.com\r\n\r\n");
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/', array(), '', '1.0');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->end();
+ }
+
+ /** @test */
+ public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionCloseHeader()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n");
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->end();
+ }
+
+ /** @test */
+ public function getOptionsAsteriskShouldSendAOptionsRequestAsteriskRequestTarget()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("OPTIONS * HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n");
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('OPTIONS', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1');
+ $requestData = $requestData->withRequestTarget('*');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->end();
+ }
+
+ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsContentLengthZero()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n");
+ $connection->expects($this->once())->method('close');
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $that = $this;
+ $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) {
+ $body->on('data', $that->expectCallableNever());
+ $body->on('end', $that->expectCallableOnce());
+ $body->on('close', $that->expectCallableOnce());
+ });
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
+ }
+
+ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsStatusNoContent()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n");
+ $connection->expects($this->once())->method('close');
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $that = $this;
+ $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) {
+ $body->on('data', $that->expectCallableNever());
+ $body->on('end', $that->expectCallableOnce());
+ $body->on('close', $that->expectCallableOnce());
+ });
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.1 204 No Content\r\n\r\n");
+ }
+
+ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsStatusNotModifiedWithContentLengthGiven()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n");
+ $connection->expects($this->once())->method('close');
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $that = $this;
+ $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) {
+ $body->on('data', $that->expectCallableNever());
+ $body->on('end', $that->expectCallableOnce());
+ $body->on('close', $that->expectCallableOnce());
+ });
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.1 304 Not Modified\r\nContent-Length: 100\r\n\r\n");
+ }
+
+ public function testStreamShouldEmitResponseWithEmptyBodyWhenRequestMethodIsHead()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("HEAD / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n");
+ $connection->expects($this->once())->method('close');
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('HEAD', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $that = $this;
+ $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) {
+ $body->on('data', $that->expectCallableNever());
+ $body->on('end', $that->expectCallableOnce());
+ $body->on('close', $that->expectCallableOnce());
+ });
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n");
+ }
+
+ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenResponseContainsContentLengthAndResponseBody()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n");
+ $connection->expects($this->once())->method('close');
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $that = $this;
+ $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) {
+ $body->on('data', $that->expectCallableOnceWith('OK'));
+ $body->on('end', $that->expectCallableOnce());
+ $body->on('close', $that->expectCallableOnce());
+ });
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK");
+ }
+
+ public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResponseContainsContentLengthWithoutResponseBody()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n");
+ $connection->expects($this->never())->method('close');
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $that = $this;
+ $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) {
+ $body->on('data', $that->expectCallableNever());
+ $body->on('end', $that->expectCallableNever());
+ $body->on('close', $that->expectCallableNever());
+ });
+ $request->on('close', $this->expectCallableNever());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\n");
+ }
+
+ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndWhenResponseContainsContentLengthWithIncompleteResponseBody()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n");
+ $connection->expects($this->never())->method('close');
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $that = $this;
+ $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) {
+ $body->on('data', $that->expectCallableOnce('O'));
+ $body->on('end', $that->expectCallableNever());
+ $body->on('close', $that->expectCallableNever());
+ });
+ $request->on('close', $this->expectCallableNever());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nO");
+ }
+
+ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenResponseContainsTransferEncodingChunkedAndResponseBody()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n");
+ $connection->expects($this->once())->method('close');
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $that = $this;
+ $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) {
+ $body->on('data', $that->expectCallableOnceWith('OK'));
+ $body->on('end', $that->expectCallableOnce());
+ $body->on('close', $that->expectCallableOnce());
+ });
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nOK\r\n0\r\n\r\n");
+ }
+
+ public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResponseContainsTransferEncodingChunkedWithoutResponseBody()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n");
+ $connection->expects($this->never())->method('close');
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $that = $this;
+ $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) {
+ $body->on('data', $that->expectCallableNever());
+ $body->on('end', $that->expectCallableNever());
+ $body->on('close', $that->expectCallableNever());
+ });
+ $request->on('close', $this->expectCallableNever());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n");
+ }
+
+ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndWhenResponseContainsTransferEncodingChunkedWithIncompleteResponseBody()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n");
+ $connection->expects($this->never())->method('close');
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $that = $this;
+ $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) {
+ $body->on('data', $that->expectCallableOnceWith('O'));
+ $body->on('end', $that->expectCallableNever());
+ $body->on('close', $that->expectCallableNever());
+ });
+ $request->on('close', $this->expectCallableNever());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nO");
+ }
+
+ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndWhenResponseContainsNoContentLengthAndIncompleteResponseBody()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n");
+ $connection->expects($this->never())->method('close');
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $that = $this;
+ $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) {
+ $body->on('data', $that->expectCallableOnce('O'));
+ $body->on('end', $that->expectCallableNever());
+ $body->on('close', $that->expectCallableNever());
+ });
+ $request->on('close', $this->expectCallableNever());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.1 200 OK\r\n\r\nO");
+ }
+
+ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenResponseContainsNoContentLengthAndResponseBodyTerminatedByConnectionEndEvent()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n");
+ $connection->expects($this->once())->method('close');
+
+ $endEvent = null;
+ $eventName = null;
+ $connection->expects($this->any())->method('on')->with($this->callback(function ($name) use (&$eventName) {
+ $eventName = $name;
+ return true;
+ }), $this->callback(function ($cb) use (&$endEvent, &$eventName) {
+ if ($eventName === 'end') {
+ $endEvent = $cb;
+ }
+ return true;
+ }));
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $that = $this;
+ $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) {
+ $body->on('data', $that->expectCallableOnce('OK'));
+ $body->on('end', $that->expectCallableOnce());
+ $body->on('close', $that->expectCallableOnce());
+ });
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.1 200 OK\r\n\r\nOK");
+
+ $this->assertNotNull($endEvent);
+ call_user_func($endEvent); // $endEvent() (PHP 5.4+)
+ }
+
+ public function testStreamShouldReuseConnectionForHttp11ByDefault()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n");
+ $connection->expects($this->once())->method('isReadable')->willReturn(true);
+ $connection->expects($this->never())->method('close');
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+ $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com/'), $connection);
+
+ $requestData = new Request('GET', 'http://www.example.com/', array(), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
+ }
+
+ public function testStreamShouldNotReuseConnectionWhenResponseContainsConnectionClose()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n");
+ $connection->expects($this->once())->method('isReadable')->willReturn(true);
+ $connection->expects($this->once())->method('close');
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/', array(), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
+ }
+
+ public function testStreamShouldNotReuseConnectionWhenRequestContainsConnectionCloseWithAdditionalOptions()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: FOO, CLOSE, BAR\r\n\r\n");
+ $connection->expects($this->once())->method('isReadable')->willReturn(true);
+ $connection->expects($this->once())->method('close');
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'FOO, CLOSE, BAR'), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: Foo, Close, Bar\r\n\r\n");
+ }
+
+ public function testStreamShouldNotReuseConnectionForHttp10ByDefault()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.0\r\nHost: www.example.com\r\n\r\n");
+ $connection->expects($this->once())->method('isReadable')->willReturn(true);
+ $connection->expects($this->once())->method('close');
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/', array(), '', '1.0');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.0 200 OK\r\nContent-Length: 0\r\n\r\n");
+ }
+
+ public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndResponseContainConnectionKeepAlive()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.0\r\nHost: www.example.com\r\nConnection: keep-alive\r\n\r\n");
+ $connection->expects($this->once())->method('isReadable')->willReturn(true);
+ $connection->expects($this->never())->method('close');
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+ $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com/'), $connection);
+
+ $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'keep-alive'), '', '1.0');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.0 200 OK\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n");
+ }
+
+ public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndResponseContainConnectionKeepAliveWithAdditionalOptions()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.0\r\nHost: www.example.com\r\nConnection: FOO, KEEP-ALIVE, BAR\r\n\r\n");
+ $connection->expects($this->once())->method('isReadable')->willReturn(true);
+ $connection->expects($this->never())->method('close');
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+ $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com/'), $connection);
+
+ $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'FOO, KEEP-ALIVE, BAR'), '', '1.0');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.0 200 OK\r\nContent-Length: 0\r\nConnection: Foo, Keep-Alive, Bar\r\n\r\n");
+ }
+
+ public function testStreamShouldNotReuseConnectionWhenResponseContainsNoContentLengthAndResponseBodyTerminatedByConnectionEndEvent()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n");
+ $connection->expects($this->once())->method('isReadable')->willReturn(false);
+ $connection->expects($this->once())->method('close');
+
+ $endEvent = null;
+ $eventName = null;
+ $connection->expects($this->any())->method('on')->with($this->callback(function ($name) use (&$eventName) {
+ $eventName = $name;
+ return true;
+ }), $this->callback(function ($cb) use (&$endEvent, &$eventName) {
+ if ($eventName === 'end') {
+ $endEvent = $cb;
+ }
+ return true;
+ }));
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/', array(), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.1 200 OK\r\n\r\n");
+
+ $this->assertNotNull($endEvent);
+ call_user_func($endEvent); // $endEvent() (PHP 5.4+)
+ }
+
+ public function testStreamShouldNotReuseConnectionWhenResponseContainsContentLengthButIsTerminatedByUnexpectedCloseEvent()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n");
+ $connection->expects($this->atMost(1))->method('isReadable')->willReturn(false);
+ $connection->expects($this->once())->method('close');
+
+ $closeEvent = null;
+ $eventName = null;
+ $connection->expects($this->any())->method('on')->with($this->callback(function ($name) use (&$eventName) {
+ $eventName = $name;
+ return true;
+ }), $this->callback(function ($cb) use (&$closeEvent, &$eventName) {
+ if ($eventName === 'close') {
+ $closeEvent = $cb;
+ }
+ return true;
+ }));
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/', array(), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\n");
+
+ $this->assertNotNull($closeEvent);
+ call_user_func($closeEvent); // $closeEvent() (PHP 5.4+)
+ }
+
+ public function testStreamShouldReuseConnectionWhenResponseContainsTransferEncodingChunkedAndResponseBody()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n");
+ $connection->expects($this->once())->method('isReadable')->willReturn(true);
+ $connection->expects($this->never())->method('close');
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+ $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com/'), $connection);
+
+ $requestData = new Request('GET', 'http://www.example.com/', array(), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nOK\r\n0\r\n\r\n");
+ }
+
+ public function testStreamShouldNotReuseConnectionWhenResponseContainsTransferEncodingChunkedAndResponseBodyContainsInvalidData()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n");
+ $connection->expects($this->atMost(1))->method('isReadable')->willReturn(true);
+ $connection->expects($this->once())->method('close');
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/', array(), '', '1.1');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->end();
+
+ $request->handleData("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nINVALID\r\n");
+ }
+
+ /** @test */
+ public function postRequestShouldSendAPostRequest()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->once())->method('write')->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome post data$#"));
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->end('some post data');
+
+ $request->handleData("HTTP/1.0 200 OK\r\n");
+ $request->handleData("Content-Type: text/plain\r\n");
+ $request->handleData("\r\nbody");
+ }
+
+ /** @test */
+ public function writeWithAPostRequestShouldSendToTheStream()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->exactly(3))->method('write')->withConsecutive(
+ array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")),
+ array($this->identicalTo("post")),
+ array($this->identicalTo("data"))
+ );
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->write("some");
+ $request->write("post");
+ $request->end("data");
+
+ $request->handleData("HTTP/1.0 200 OK\r\n");
+ $request->handleData("Content-Type: text/plain\r\n");
+ $request->handleData("\r\nbody");
+ }
+
+ /** @test */
+ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->exactly(2))->method('write')->withConsecutive(
+ array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsomepost$#")),
+ array($this->identicalTo("data"))
+ )->willReturn(
+ true
+ );
+
+ $deferred = new Deferred();
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn($deferred->promise());
+
+ $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $this->assertFalse($request->write("some"));
+ $this->assertFalse($request->write("post"));
+
+ $request->on('drain', $this->expectCallableOnce());
+ $request->once('drain', function () use ($request) {
+ $request->write("data");
+ $request->end();
+ });
+
+ $deferred->resolve($connection);
+
+ $request->handleData("HTTP/1.0 200 OK\r\n");
+ $request->handleData("Content-Type: text/plain\r\n");
+ $request->handleData("\r\nbody");
+ }
+
+ /** @test */
+ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsBuffer()
+ {
+ $connection = $this->getMockBuilder('React\Socket\Connection')
+ ->disableOriginalConstructor()
+ ->setMethods(array('write'))
+ ->getMock();
+
+ $connection->expects($this->exactly(2))->method('write')->withConsecutive(
+ array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsomepost$#")),
+ array($this->identicalTo("data"))
+ )->willReturn(
+ false
+ );
+
+ $deferred = new Deferred();
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn($deferred->promise());
+
+ $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $this->assertFalse($request->write("some"));
+ $this->assertFalse($request->write("post"));
+
+ $request->on('drain', $this->expectCallableOnce());
+ $request->once('drain', function () use ($request) {
+ $request->write("data");
+ $request->end();
+ });
+
+ $deferred->resolve($connection);
+ $connection->emit('drain');
+
+ $request->handleData("HTTP/1.0 200 OK\r\n");
+ $request->handleData("Content-Type: text/plain\r\n");
+ $request->handleData("\r\nbody");
+ }
+
+ /** @test */
+ public function pipeShouldPipeDataIntoTheRequestBody()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+ $connection->expects($this->exactly(3))->method('write')->withConsecutive(
+ array($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")),
+ array($this->identicalTo("post")),
+ array($this->identicalTo("data"))
+ );
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $loop = $this
+ ->getMockBuilder('React\EventLoop\LoopInterface')
+ ->getMock();
+
+ $stream = fopen('php://memory', 'r+');
+ $stream = new DuplexResourceStream($stream, $loop);
+
+ $stream->pipe($request);
+ $stream->emit('data', array('some'));
+ $stream->emit('data', array('post'));
+ $stream->emit('data', array('data'));
+
+ $request->handleData("HTTP/1.0 200 OK\r\n");
+ $request->handleData("Content-Type: text/plain\r\n");
+ $request->handleData("\r\nbody");
+ }
+
+ /**
+ * @test
+ */
+ public function writeShouldStartConnecting()
+ {
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(new Promise(function () { }));
+
+ $requestData = new Request('POST', 'http://www.example.com/');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->write('test');
+ }
+
+ /**
+ * @test
+ */
+ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode()
+ {
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(new Promise(function () { }));
+
+ $requestData = new Request('POST', 'http://www.example.com/');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->end();
+
+ $this->assertFalse($request->isWritable());
+ }
+
+ /**
+ * @test
+ */
+ public function closeShouldEmitCloseEvent()
+ {
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+
+ $requestData = new Request('POST', 'http://www.example.com/');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->on('close', $this->expectCallableOnce());
+ $request->close();
+ }
+
+ /**
+ * @test
+ */
+ public function writeAfterCloseReturnsFalse()
+ {
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+
+ $requestData = new Request('POST', 'http://www.example.com/');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->close();
+
+ $this->assertFalse($request->isWritable());
+ $this->assertFalse($request->write('nope'));
+ }
+
+ /**
+ * @test
+ */
+ public function endAfterCloseIsNoOp()
+ {
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->never())->method('connect');
+
+ $requestData = new Request('POST', 'http://www.example.com/');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->close();
+ $request->end();
+ }
+
+ /**
+ * @test
+ */
+ public function closeShouldCancelPendingConnectionAttempt()
+ {
+ $promise = new Promise(function () {}, function () {
+ throw new \RuntimeException();
+ });
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn($promise);
+
+ $requestData = new Request('POST', 'http://www.example.com/');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->end();
+
+ $request->on('error', $this->expectCallableNever());
+ $request->on('close', $this->expectCallableOnce());
+
+ $request->close();
+ $request->close();
+ }
+
+ /** @test */
+ public function requestShouldRemoveAllListenerAfterClosed()
+ {
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+
+ $requestData = new Request('GET', 'http://www.example.com/');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $request->on('close', function () {});
+ $this->assertCount(1, $request->listeners('close'));
+
+ $request->close();
+ $this->assertCount(0, $request->listeners('close'));
+ }
+
+ /** @test */
+ public function multivalueHeader()
+ {
+ $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
+
+ $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock();
+ $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
+
+ $requestData = new Request('GET', 'http://www.example.com/');
+ $request = new ClientRequestStream($connectionManager, $requestData);
+
+ $response = null;
+ $request->on('response', $this->expectCallableOnce());
+ $request->on('response', function ($value) use (&$response) {
+ $response = $value;
+ });
+
+ $request->end();
+
+ $request->handleData("HTTP/1.0 200 OK\r\n");
+ $request->handleData("Content-Type: text/plain\r\n");
+ $request->handleData("X-Xss-Protection:1; mode=block\r\n");
+ $request->handleData("Cache-Control:public, must-revalidate, max-age=0\r\n");
+ $request->handleData("\r\nbody");
+
+ /** @var \Psr\Http\Message\ResponseInterface $response */
+ $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $response);
+ $this->assertEquals('1.0', $response->getProtocolVersion());
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('OK', $response->getReasonPhrase());
+ $this->assertEquals('text/plain', $response->getHeaderLine('Content-Type'));
+ $this->assertEquals('1; mode=block', $response->getHeaderLine('X-Xss-Protection'));
+ $this->assertEquals('public, must-revalidate, max-age=0', $response->getHeaderLine('Cache-Control'));
+ }
+}
diff --git a/tests/Io/ClockTest.php b/tests/Io/ClockTest.php
new file mode 100644
index 00000000..8f4b90fa
--- /dev/null
+++ b/tests/Io/ClockTest.php
@@ -0,0 +1,43 @@
+getMockBuilder('React\EventLoop\LoopInterface')->getMock();
+
+ $clock = new Clock($loop);
+
+ $now = $clock->now();
+ $this->assertTrue(is_float($now)); // assertIsFloat() on PHPUnit 8+
+ $this->assertEquals($now, $clock->now());
+ }
+
+ public function testNowResetsMemoizedTimestampOnFutureTick()
+ {
+ $tick = null;
+ $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
+ $loop->expects($this->once())->method('futureTick')->with($this->callback(function ($cb) use (&$tick) {
+ $tick = $cb;
+ return true;
+ }));
+
+ $clock = new Clock($loop);
+
+ $now = $clock->now();
+
+ $ref = new \ReflectionProperty($clock, 'now');
+ $ref->setAccessible(true);
+ $this->assertEquals($now, $ref->getValue($clock));
+
+ $this->assertNotNull($tick);
+ $tick();
+
+ $this->assertNull($ref->getValue($clock));
+ }
+}
diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php
index d8f5f232..762d7bdb 100644
--- a/tests/Io/MiddlewareRunnerTest.php
+++ b/tests/Io/MiddlewareRunnerTest.php
@@ -2,18 +2,16 @@
namespace React\Tests\Http\Io;
-use Clue\React\Block;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Io\MiddlewareRunner;
+use React\Http\Message\Response;
use React\Http\Message\ServerRequest;
use React\Promise;
-use React\Promise\CancellablePromiseInterface;
use React\Promise\PromiseInterface;
use React\Tests\Http\Middleware\ProcessStack;
use React\Tests\Http\TestCase;
-use RingCentral\Psr7\Response;
final class MiddlewareRunnerTest extends TestCase
{
@@ -161,7 +159,7 @@ public function testProcessStack(array $middlewares, $expectedCallCount)
$response = $middlewareStack($request);
$this->assertTrue($response instanceof PromiseInterface);
- $response = Block\await($response);
+ $response = \React\Async\await($response);
$this->assertTrue($response instanceof ResponseInterface);
$this->assertSame(200, $response->getStatusCode());
@@ -228,7 +226,7 @@ function () use ($errorHandler, &$called, $response, $exception) {
$request = new ServerRequest('GET', 'https://example.com/');
- $this->assertSame($response, Block\await($runner($request)));
+ $this->assertSame($response, \React\Async\await($runner($request)));
$this->assertSame(1, $retryCalled);
$this->assertSame(2, $called);
$this->assertSame($exception, $error);
@@ -480,7 +478,7 @@ function (RequestInterface $request) use ($once) {
$promise = $middleware($request);
- $this->assertTrue($promise instanceof CancellablePromiseInterface);
+ $this->assertTrue($promise instanceof PromiseInterface && \method_exists($promise, 'cancel'));
$promise->cancel();
}
}
diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php
index 14550f57..7f1ec667 100644
--- a/tests/Io/MultipartParserTest.php
+++ b/tests/Io/MultipartParserTest.php
@@ -1026,4 +1026,44 @@ public function testPostMaxFileSizeIgnoredByFilesComingBeforeIt()
$this->assertTrue(isset($files['file4']));
$this->assertSame(UPLOAD_ERR_OK, $files['file4']->getError());
}
-}
\ No newline at end of file
+
+ public function testWeOnlyParseTheAmountOfMultiPartChunksWeConfigured()
+ {
+ $chunkCount = 5000000;
+ $boundary = "---------------------------12758086162038677464950549563";
+
+ $chunk = "--$boundary\r\n";
+ $chunk .= "Content-Disposition: form-data; name=\"f\"\r\n";
+ $chunk .= "\r\n";
+ $chunk .= "u\r\n";
+ $data = '';
+ $data .= str_repeat($chunk, $chunkCount);
+ $data .= "--$boundary--\r\n";
+
+ $request = new ServerRequest('POST', 'http://example.com/', array(
+ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
+ ), $data, 1.1);
+
+ $parser = new MultipartParser();
+
+ $reflectecClass = new \ReflectionClass('\React\Http\Io\MultipartParser');
+ $requestProperty = $reflectecClass->getProperty('request');
+ $requestProperty->setAccessible(true);
+ $cursorProperty = $reflectecClass->getProperty('cursor');
+ $cursorProperty->setAccessible(true);
+ $multipartBodyPartCountProperty = $reflectecClass->getProperty('multipartBodyPartCount');
+ $multipartBodyPartCountProperty->setAccessible(true);
+ $maxMultipartBodyPartsProperty = $reflectecClass->getProperty('maxMultipartBodyParts');
+ $maxMultipartBodyPartsProperty->setAccessible(true);
+ $parseBodyMethod = $reflectecClass->getMethod('parseBody');
+ $parseBodyMethod->setAccessible(true);
+
+ $this->assertSame(0, $cursorProperty->getValue($parser));
+
+ $requestProperty->setValue($parser, $request);
+ $parseBodyMethod->invoke($parser, '--' . $boundary, $data);
+
+ $this->assertSame(strlen(str_repeat($chunk, $multipartBodyPartCountProperty->getValue($parser))), $cursorProperty->getValue($parser) + 2);
+ $this->assertSame($multipartBodyPartCountProperty->getValue($parser), $maxMultipartBodyPartsProperty->getValue($parser) + 1);
+ }
+}
diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php
index 356443fb..87d6bf1b 100644
--- a/tests/Io/RequestHeaderParserTest.php
+++ b/tests/Io/RequestHeaderParserTest.php
@@ -2,15 +2,17 @@
namespace React\Tests\Http\Io;
+use Psr\Http\Message\ServerRequestInterface;
use React\Http\Io\RequestHeaderParser;
use React\Tests\Http\TestCase;
-use Psr\Http\Message\ServerRequestInterface;
class RequestHeaderParserTest extends TestCase
{
public function testSplitShouldHappenOnDoubleCrlf()
{
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', $this->expectCallableNever());
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock();
@@ -29,7 +31,9 @@ public function testSplitShouldHappenOnDoubleCrlf()
public function testFeedInOneGo()
{
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', $this->expectCallableOnce());
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock();
@@ -41,7 +45,9 @@ public function testFeedInOneGo()
public function testFeedTwoRequestsOnSeparateConnections()
{
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$called = 0;
$parser->on('headers', function () use (&$called) {
@@ -65,7 +71,9 @@ public function testHeadersEventShouldEmitRequestAndConnection()
$request = null;
$conn = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', function ($parsedRequest, $connection) use (&$request, &$conn) {
$request = $parsedRequest;
$conn = $connection;
@@ -88,7 +96,9 @@ public function testHeadersEventShouldEmitRequestAndConnection()
public function testHeadersEventShouldEmitRequestWhichShouldEmitEndForStreamingBodyWithoutContentLengthFromInitialRequestBody()
{
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$ended = false;
$that = $this;
@@ -112,7 +122,9 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitEndForStreamingB
public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDataFromInitialRequestBody()
{
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$buffer = '';
$that = $this;
@@ -140,7 +152,9 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDat
public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyWithPlentyOfDataFromInitialRequestBody()
{
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$buffer = '';
$that = $this;
@@ -166,7 +180,9 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyWit
public function testHeadersEventShouldEmitRequestWhichShouldNotEmitStreamingBodyDataWithoutContentLengthFromInitialRequestBody()
{
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$buffer = '';
$that = $this;
@@ -191,7 +207,9 @@ public function testHeadersEventShouldEmitRequestWhichShouldNotEmitStreamingBody
public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDataUntilContentLengthBoundaryFromInitialRequestBody()
{
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$buffer = '';
$that = $this;
@@ -218,7 +236,9 @@ public function testHeadersEventShouldParsePathAndQueryString()
{
$request = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', function ($parsedRequest) use (&$request) {
$request = $parsedRequest;
});
@@ -245,7 +265,9 @@ public function testHeaderEventWithShouldApplyDefaultAddressFromLocalConnectionA
{
$request = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', function ($parsedRequest) use (&$request) {
$request = $parsedRequest;
});
@@ -264,7 +286,9 @@ public function testHeaderEventViaHttpsShouldApplyHttpsSchemeFromLocalTlsConnect
{
$request = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', function ($parsedRequest) use (&$request) {
$request = $parsedRequest;
});
@@ -284,7 +308,9 @@ public function testHeaderOverflowShouldEmitError()
$error = null;
$passedConnection = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', $this->expectCallableNever());
$parser->on('error', function ($message, $connection) use (&$error, &$passedConnection) {
$error = $message;
@@ -306,7 +332,9 @@ public function testInvalidEmptyRequestHeadersParseException()
{
$error = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', $this->expectCallableNever());
$parser->on('error', function ($message) use (&$error) {
$error = $message;
@@ -325,7 +353,9 @@ public function testInvalidMalformedRequestLineParseException()
{
$error = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', $this->expectCallableNever());
$parser->on('error', function ($message) use (&$error) {
$error = $message;
@@ -344,7 +374,9 @@ public function testInvalidMalformedRequestHeadersThrowsParseException()
{
$error = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', $this->expectCallableNever());
$parser->on('error', function ($message) use (&$error) {
$error = $message;
@@ -363,7 +395,9 @@ public function testInvalidMalformedRequestHeadersWhitespaceThrowsParseException
{
$error = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', $this->expectCallableNever());
$parser->on('error', function ($message) use (&$error) {
$error = $message;
@@ -382,7 +416,9 @@ public function testInvalidAbsoluteFormSchemeEmitsError()
{
$error = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', $this->expectCallableNever());
$parser->on('error', function ($message) use (&$error) {
$error = $message;
@@ -401,7 +437,9 @@ public function testOriginFormWithSchemeSeparatorInParam()
{
$request = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('error', $this->expectCallableNever());
$parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request) {
$request = $parsedRequest;
@@ -426,7 +464,9 @@ public function testUriStartingWithColonSlashSlashFails()
{
$error = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', $this->expectCallableNever());
$parser->on('error', function ($message) use (&$error) {
$error = $message;
@@ -445,7 +485,9 @@ public function testInvalidAbsoluteFormWithFragmentEmitsError()
{
$error = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', $this->expectCallableNever());
$parser->on('error', function ($message) use (&$error) {
$error = $message;
@@ -464,7 +506,9 @@ public function testInvalidHeaderContainsFullUri()
{
$error = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', $this->expectCallableNever());
$parser->on('error', function ($message) use (&$error) {
$error = $message;
@@ -483,7 +527,9 @@ public function testInvalidAbsoluteFormWithHostHeaderEmpty()
{
$error = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', $this->expectCallableNever());
$parser->on('error', function ($message) use (&$error) {
$error = $message;
@@ -502,7 +548,9 @@ public function testInvalidConnectRequestWithNonAuthorityForm()
{
$error = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', $this->expectCallableNever());
$parser->on('error', function ($message) use (&$error) {
$error = $message;
@@ -521,7 +569,9 @@ public function testInvalidHttpVersion()
{
$error = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', $this->expectCallableNever());
$parser->on('error', function ($message) use (&$error) {
$error = $message;
@@ -541,7 +591,9 @@ public function testInvalidContentLengthRequestHeaderWillEmitError()
{
$error = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', $this->expectCallableNever());
$parser->on('error', function ($message) use (&$error) {
$error = $message;
@@ -561,7 +613,9 @@ public function testInvalidRequestWithMultipleContentLengthRequestHeadersWillEmi
{
$error = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', $this->expectCallableNever());
$parser->on('error', function ($message) use (&$error) {
$error = $message;
@@ -581,7 +635,9 @@ public function testInvalidTransferEncodingRequestHeaderWillEmitError()
{
$error = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', $this->expectCallableNever());
$parser->on('error', function ($message) use (&$error) {
$error = $message;
@@ -601,7 +657,9 @@ public function testInvalidRequestWithBothTransferEncodingAndContentLengthWillEm
{
$error = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', $this->expectCallableNever());
$parser->on('error', function ($message) use (&$error) {
$error = $message;
@@ -621,7 +679,10 @@ public function testServerParamsWillBeSetOnHttpsRequest()
{
$request = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+ $clock->expects($this->once())->method('now')->willReturn(1652972091.3958);
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', function ($parsedRequest) use (&$request) {
$request = $parsedRequest;
@@ -637,8 +698,8 @@ public function testServerParamsWillBeSetOnHttpsRequest()
$serverParams = $request->getServerParams();
$this->assertEquals('on', $serverParams['HTTPS']);
- $this->assertNotEmpty($serverParams['REQUEST_TIME']);
- $this->assertNotEmpty($serverParams['REQUEST_TIME_FLOAT']);
+ $this->assertEquals(1652972091, $serverParams['REQUEST_TIME']);
+ $this->assertEquals(1652972091.3958, $serverParams['REQUEST_TIME_FLOAT']);
$this->assertEquals('127.1.1.1', $serverParams['SERVER_ADDR']);
$this->assertEquals('8000', $serverParams['SERVER_PORT']);
@@ -651,7 +712,10 @@ public function testServerParamsWillBeSetOnHttpRequest()
{
$request = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+ $clock->expects($this->once())->method('now')->willReturn(1652972091.3958);
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', function ($parsedRequest) use (&$request) {
$request = $parsedRequest;
@@ -667,8 +731,8 @@ public function testServerParamsWillBeSetOnHttpRequest()
$serverParams = $request->getServerParams();
$this->assertArrayNotHasKey('HTTPS', $serverParams);
- $this->assertNotEmpty($serverParams['REQUEST_TIME']);
- $this->assertNotEmpty($serverParams['REQUEST_TIME_FLOAT']);
+ $this->assertEquals(1652972091, $serverParams['REQUEST_TIME']);
+ $this->assertEquals(1652972091.3958, $serverParams['REQUEST_TIME_FLOAT']);
$this->assertEquals('127.1.1.1', $serverParams['SERVER_ADDR']);
$this->assertEquals('8000', $serverParams['SERVER_PORT']);
@@ -681,7 +745,10 @@ public function testServerParamsWillNotSetRemoteAddressForUnixDomainSockets()
{
$request = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+ $clock->expects($this->once())->method('now')->willReturn(1652972091.3958);
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', function ($parsedRequest) use (&$request) {
$request = $parsedRequest;
@@ -697,8 +764,8 @@ public function testServerParamsWillNotSetRemoteAddressForUnixDomainSockets()
$serverParams = $request->getServerParams();
$this->assertArrayNotHasKey('HTTPS', $serverParams);
- $this->assertNotEmpty($serverParams['REQUEST_TIME']);
- $this->assertNotEmpty($serverParams['REQUEST_TIME_FLOAT']);
+ $this->assertEquals(1652972091, $serverParams['REQUEST_TIME']);
+ $this->assertEquals(1652972091.3958, $serverParams['REQUEST_TIME_FLOAT']);
$this->assertArrayNotHasKey('SERVER_ADDR', $serverParams);
$this->assertArrayNotHasKey('SERVER_PORT', $serverParams);
@@ -715,7 +782,10 @@ public function testServerParamsWontBeSetOnMissingUrls()
$request = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+ $clock->expects($this->once())->method('now')->willReturn(1652972091.3958);
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', function ($parsedRequest) use (&$request) {
$request = $parsedRequest;
@@ -728,8 +798,8 @@ public function testServerParamsWontBeSetOnMissingUrls()
$serverParams = $request->getServerParams();
- $this->assertNotEmpty($serverParams['REQUEST_TIME']);
- $this->assertNotEmpty($serverParams['REQUEST_TIME_FLOAT']);
+ $this->assertEquals(1652972091, $serverParams['REQUEST_TIME']);
+ $this->assertEquals(1652972091.3958, $serverParams['REQUEST_TIME_FLOAT']);
$this->assertArrayNotHasKey('SERVER_ADDR', $serverParams);
$this->assertArrayNotHasKey('SERVER_PORT', $serverParams);
@@ -738,11 +808,69 @@ public function testServerParamsWontBeSetOnMissingUrls()
$this->assertArrayNotHasKey('REMOTE_PORT', $serverParams);
}
+ public function testServerParamsWillBeReusedForMultipleRequestsFromSameConnection()
+ {
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+ $clock->expects($this->exactly(2))->method('now')->willReturn(1652972091.3958);
+
+ $parser = new RequestHeaderParser($clock);
+
+ $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress', 'getRemoteAddress'))->getMock();
+ $connection->expects($this->once())->method('getLocalAddress')->willReturn('tcp://127.1.1.1:8000');
+ $connection->expects($this->once())->method('getRemoteAddress')->willReturn('tcp://192.168.1.1:8001');
+
+ $parser->handle($connection);
+ $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"));
+
+ $request = null;
+ $parser->on('headers', function ($parsedRequest) use (&$request) {
+ $request = $parsedRequest;
+ });
+
+ $parser->handle($connection);
+ $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"));
+
+ assert($request instanceof ServerRequestInterface);
+ $serverParams = $request->getServerParams();
+
+ $this->assertArrayNotHasKey('HTTPS', $serverParams);
+ $this->assertEquals(1652972091, $serverParams['REQUEST_TIME']);
+ $this->assertEquals(1652972091.3958, $serverParams['REQUEST_TIME_FLOAT']);
+
+ $this->assertEquals('127.1.1.1', $serverParams['SERVER_ADDR']);
+ $this->assertEquals('8000', $serverParams['SERVER_PORT']);
+
+ $this->assertEquals('192.168.1.1', $serverParams['REMOTE_ADDR']);
+ $this->assertEquals('8001', $serverParams['REMOTE_PORT']);
+ }
+
+ public function testServerParamsWillBeRememberedUntilConnectionIsClosed()
+ {
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
+
+ $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress', 'getRemoteAddress'))->getMock();
+
+ $parser->handle($connection);
+ $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"));
+
+ $ref = new \ReflectionProperty($parser, 'connectionParams');
+ $ref->setAccessible(true);
+
+ $this->assertCount(1, $ref->getValue($parser));
+
+ $connection->emit('close');
+ $this->assertEquals(array(), $ref->getValue($parser));
+ }
+
public function testQueryParmetersWillBeSet()
{
$request = null;
- $parser = new RequestHeaderParser();
+ $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock();
+
+ $parser = new RequestHeaderParser($clock);
$parser->on('headers', function ($parsedRequest) use (&$request) {
$request = $parsedRequest;
diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php
index 1c6d1d6b..a65b13b3 100644
--- a/tests/Io/SenderTest.php
+++ b/tests/Io/SenderTest.php
@@ -2,18 +2,20 @@
namespace React\Tests\Http\Io;
-use Clue\React\Block;
+use Psr\Http\Message\RequestInterface;
use React\Http\Client\Client as HttpClient;
-use React\Http\Client\RequestData;
+use React\Http\Io\ClientConnectionManager;
+use React\Http\Io\EmptyBodyStream;
use React\Http\Io\ReadableBodyStream;
use React\Http\Io\Sender;
+use React\Http\Message\Request;
use React\Promise;
use React\Stream\ThroughStream;
use React\Tests\Http\TestCase;
-use RingCentral\Psr7\Request;
class SenderTest extends TestCase
{
+ /** @var \React\EventLoop\LoopInterface */
private $loop;
/**
@@ -26,7 +28,9 @@ public function setUpLoop()
public function testCreateFromLoop()
{
- $sender = Sender::createFromLoop($this->loop, null);
+ $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
+
+ $sender = Sender::createFromLoop($this->loop, $connector);
$this->assertInstanceOf('React\Http\Io\Sender', $sender);
}
@@ -36,14 +40,18 @@ public function testSenderRejectsInvalidUri()
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
$connector->expects($this->never())->method('connect');
- $sender = new Sender(new HttpClient($this->loop, $connector));
+ $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop)));
$request = new Request('GET', 'www.google.com');
$promise = $sender->send($request);
- $this->setExpectedException('InvalidArgumentException');
- Block\await($promise, $this->loop);
+ $exception = null;
+ $promise->then(null, function ($e) use (&$exception) {
+ $exception = $e;
+ });
+
+ $this->assertInstanceOf('InvalidArgumentException', $exception);
}
public function testSenderConnectorRejection()
@@ -51,25 +59,26 @@ public function testSenderConnectorRejection()
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
$connector->expects($this->once())->method('connect')->willReturn(Promise\reject(new \RuntimeException('Rejected')));
- $sender = new Sender(new HttpClient($this->loop, $connector));
+ $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop)));
$request = new Request('GET', 'http://www.google.com/');
$promise = $sender->send($request);
- $this->setExpectedException('RuntimeException');
- Block\await($promise, $this->loop);
+ $exception = null;
+ $promise->then(null, function ($e) use (&$exception) {
+ $exception = $e;
+ });
+
+ $this->assertInstanceOf('RuntimeException', $exception);
}
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);
@@ -80,12 +89,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);
@@ -95,16 +101,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);
@@ -115,7 +118,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);
@@ -134,7 +137,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);
@@ -153,7 +156,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');
@@ -183,7 +186,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');
@@ -211,7 +214,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');
@@ -240,12 +243,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);
@@ -257,12 +257,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);
@@ -270,15 +267,27 @@ public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody()
$sender->send($request);
}
+ public function testSendGetWithEmptyBodyStreamWillNotPassContentLengthOrTransferEncodingHeader()
+ {
+ $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock();
+ $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) {
+ return !$request->hasHeader('Content-Length') && !$request->hasHeader('Transfer-Encoding');
+ }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock());
+
+ $sender = new Sender($client);
+
+ $body = new EmptyBodyStream();
+ $request = new Request('GET', 'http://www.google.com/', array(), $body);
+
+ $sender->send($request);
+ }
+
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);
@@ -289,12 +298,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);
@@ -302,6 +308,34 @@ public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsI
$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 () {
@@ -311,15 +345,19 @@ public function testCancelRequestWillCancelConnector()
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
$connector->expects($this->once())->method('connect')->willReturn($promise);
- $sender = new Sender(new HttpClient($this->loop, $connector));
+ $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop)));
$request = new Request('GET', 'http://www.google.com/');
$promise = $sender->send($request);
$promise->cancel();
- $this->setExpectedException('RuntimeException');
- Block\await($promise, $this->loop);
+ $exception = null;
+ $promise->then(null, function ($e) use (&$exception) {
+ $exception = $e;
+ });
+
+ $this->assertInstanceOf('RuntimeException', $exception);
}
public function testCancelRequestWillCloseConnection()
@@ -330,64 +368,18 @@ public function testCancelRequestWillCloseConnection()
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
$connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($connection));
- $sender = new Sender(new HttpClient($this->loop, $connector));
+ $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop)));
$request = new Request('GET', 'http://www.google.com/');
$promise = $sender->send($request);
$promise->cancel();
- $this->setExpectedException('RuntimeException');
- Block\await($promise, $this->loop);
- }
-
- 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',
- ),
- );
- }
+ $exception = null;
+ $promise->then(null, function ($e) use (&$exception) {
+ $exception = $e;
+ });
- /**
- * @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);
+ $this->assertInstanceOf('RuntimeException', $exception);
}
}
diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php
index 45729f2b..b1410fad 100644
--- a/tests/Io/StreamingServerTest.php
+++ b/tests/Io/StreamingServerTest.php
@@ -2,6 +2,7 @@
namespace React\Tests\Http\Io;
+use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\EventLoop\Loop;
use React\Http\Io\StreamingServer;
@@ -17,14 +18,25 @@ class StreamingServerTest extends TestCase
private $connection;
private $socket;
+ /** @var ?int */
+ private $called = null;
+
/**
* @before
*/
public function setUpConnectionMockAndSocket()
{
- $this->connection = $this->getMockBuilder('React\Socket\Connection')
+ $this->connection = $this->mockConnection();
+
+ $this->socket = new SocketServerStub();
+ }
+
+
+ private function mockConnection(array $additionalMethods = array())
+ {
+ $connection = $this->getMockBuilder('React\Socket\Connection')
->disableOriginalConstructor()
- ->setMethods(
+ ->setMethods(array_merge(
array(
'write',
'end',
@@ -36,14 +48,15 @@ public function setUpConnectionMockAndSocket()
'getRemoteAddress',
'getLocalAddress',
'pipe'
- )
- )
+ ),
+ $additionalMethods
+ ))
->getMock();
- $this->connection->method('isWritable')->willReturn(true);
- $this->connection->method('isReadable')->willReturn(true);
+ $connection->method('isWritable')->willReturn(true);
+ $connection->method('isReadable')->willReturn(true);
- $this->socket = new SocketServerStub();
+ return $connection;
}
public function testRequestEventWillNotBeEmittedForIncompleteHeaders()
@@ -114,7 +127,7 @@ public function testRequestEvent()
$serverParams = $requestAssertion->getServerParams();
$this->assertSame(1, $i);
- $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion);
+ $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion);
$this->assertSame('GET', $requestAssertion->getMethod());
$this->assertSame('/', $requestAssertion->getRequestTarget());
$this->assertSame('/', $requestAssertion->getUri()->getPath());
@@ -147,7 +160,7 @@ public function testRequestEventWithSingleRequestHandlerArray()
$serverParams = $requestAssertion->getServerParams();
$this->assertSame(1, $i);
- $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion);
+ $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion);
$this->assertSame('GET', $requestAssertion->getMethod());
$this->assertSame('/', $requestAssertion->getRequestTarget());
$this->assertSame('/', $requestAssertion->getUri()->getPath());
@@ -170,7 +183,7 @@ public function testRequestGetWithHostAndCustomPort()
$data = "GET / HTTP/1.1\r\nHost: example.com:8080\r\n\r\n";
$this->connection->emit('data', array($data));
- $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion);
+ $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion);
$this->assertSame('GET', $requestAssertion->getMethod());
$this->assertSame('/', $requestAssertion->getRequestTarget());
$this->assertSame('/', $requestAssertion->getUri()->getPath());
@@ -192,7 +205,7 @@ public function testRequestGetWithHostAndHttpsPort()
$data = "GET / HTTP/1.1\r\nHost: example.com:443\r\n\r\n";
$this->connection->emit('data', array($data));
- $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion);
+ $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion);
$this->assertSame('GET', $requestAssertion->getMethod());
$this->assertSame('/', $requestAssertion->getRequestTarget());
$this->assertSame('/', $requestAssertion->getUri()->getPath());
@@ -214,7 +227,7 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored()
$data = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
$this->connection->emit('data', array($data));
- $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion);
+ $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion);
$this->assertSame('GET', $requestAssertion->getMethod());
$this->assertSame('/', $requestAssertion->getRequestTarget());
$this->assertSame('/', $requestAssertion->getUri()->getPath());
@@ -236,7 +249,7 @@ public function testRequestGetHttp10WithoutHostWillBeIgnored()
$data = "GET / HTTP/1.0\r\n\r\n";
$this->connection->emit('data', array($data));
- $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion);
+ $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion);
$this->assertSame('GET', $requestAssertion->getMethod());
$this->assertSame('/', $requestAssertion->getRequestTarget());
$this->assertSame('/', $requestAssertion->getUri()->getPath());
@@ -271,7 +284,7 @@ public function testRequestOptionsAsterisk()
$data = "OPTIONS * HTTP/1.1\r\nHost: example.com\r\n\r\n";
$this->connection->emit('data', array($data));
- $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion);
+ $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion);
$this->assertSame('OPTIONS', $requestAssertion->getMethod());
$this->assertSame('*', $requestAssertion->getRequestTarget());
$this->assertSame('', $requestAssertion->getUri()->getPath());
@@ -304,7 +317,7 @@ public function testRequestConnectAuthorityForm()
$data = "CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n";
$this->connection->emit('data', array($data));
- $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion);
+ $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion);
$this->assertSame('CONNECT', $requestAssertion->getMethod());
$this->assertSame('example.com:443', $requestAssertion->getRequestTarget());
$this->assertSame('', $requestAssertion->getUri()->getPath());
@@ -326,7 +339,7 @@ public function testRequestConnectWithoutHostWillBePassesAsIs()
$data = "CONNECT example.com:443 HTTP/1.1\r\n\r\n";
$this->connection->emit('data', array($data));
- $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion);
+ $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion);
$this->assertSame('CONNECT', $requestAssertion->getMethod());
$this->assertSame('example.com:443', $requestAssertion->getRequestTarget());
$this->assertSame('', $requestAssertion->getUri()->getPath());
@@ -348,7 +361,7 @@ public function testRequestConnectAuthorityFormWithDefaultPortWillBePassedAsIs()
$data = "CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n";
$this->connection->emit('data', array($data));
- $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion);
+ $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion);
$this->assertSame('CONNECT', $requestAssertion->getMethod());
$this->assertSame('example.com:80', $requestAssertion->getRequestTarget());
$this->assertSame('', $requestAssertion->getUri()->getPath());
@@ -370,7 +383,7 @@ public function testRequestConnectAuthorityFormNonMatchingHostWillBePassedAsIs()
$data = "CONNECT example.com:80 HTTP/1.1\r\nHost: other.example.org\r\n\r\n";
$this->connection->emit('data', array($data));
- $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion);
+ $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion);
$this->assertSame('CONNECT', $requestAssertion->getMethod());
$this->assertSame('example.com:80', $requestAssertion->getRequestTarget());
$this->assertSame('', $requestAssertion->getUri()->getPath());
@@ -422,7 +435,7 @@ public function testRequestWithoutHostEventUsesSocketAddress()
$data = "GET /test HTTP/1.0\r\n\r\n";
$this->connection->emit('data', array($data));
- $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion);
+ $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion);
$this->assertSame('GET', $requestAssertion->getMethod());
$this->assertSame('/test', $requestAssertion->getRequestTarget());
$this->assertEquals('http://127.0.0.1/test', $requestAssertion->getUri());
@@ -443,7 +456,7 @@ public function testRequestAbsoluteEvent()
$data = "GET http://example.com/test HTTP/1.1\r\nHost: example.com\r\n\r\n";
$this->connection->emit('data', array($data));
- $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion);
+ $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion);
$this->assertSame('GET', $requestAssertion->getMethod());
$this->assertSame('http://example.com/test', $requestAssertion->getRequestTarget());
$this->assertEquals('http://example.com/test', $requestAssertion->getUri());
@@ -465,7 +478,7 @@ public function testRequestAbsoluteNonMatchingHostWillBePassedAsIs()
$data = "GET http://example.com/test HTTP/1.1\r\nHost: other.example.org\r\n\r\n";
$this->connection->emit('data', array($data));
- $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion);
+ $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion);
$this->assertSame('GET', $requestAssertion->getMethod());
$this->assertSame('http://example.com/test', $requestAssertion->getRequestTarget());
$this->assertEquals('http://example.com/test', $requestAssertion->getUri());
@@ -499,7 +512,7 @@ public function testRequestOptionsAsteriskEvent()
$data = "OPTIONS * HTTP/1.1\r\nHost: example.com\r\n\r\n";
$this->connection->emit('data', array($data));
- $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion);
+ $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion);
$this->assertSame('OPTIONS', $requestAssertion->getMethod());
$this->assertSame('*', $requestAssertion->getRequestTarget());
$this->assertEquals('http://example.com/', $requestAssertion->getUri());
@@ -521,7 +534,7 @@ public function testRequestOptionsAbsoluteEvent()
$data = "OPTIONS http://example.com HTTP/1.1\r\nHost: example.com\r\n\r\n";
$this->connection->emit('data', array($data));
- $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion);
+ $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion);
$this->assertSame('OPTIONS', $requestAssertion->getMethod());
$this->assertSame('http://example.com/', $requestAssertion->getRequestTarget());
$this->assertEquals('http://example.com/', $requestAssertion->getUri());
@@ -1555,9 +1568,9 @@ function ($data) use (&$buffer) {
$this->assertInstanceOf('InvalidArgumentException', $error);
- $this->assertContainsString("HTTP/1.1 505 HTTP Version not supported\r\n", $buffer);
+ $this->assertContainsString("HTTP/1.1 505 HTTP Version Not Supported\r\n", $buffer);
$this->assertContainsString("\r\n\r\n", $buffer);
- $this->assertContainsString("Error 505: HTTP Version not supported", $buffer);
+ $this->assertContainsString("Error 505: HTTP Version Not Supported", $buffer);
}
public function testRequestOverflowWillEmitErrorAndSendErrorResponse()
@@ -2292,12 +2305,20 @@ function ($data) use (&$buffer) {
$this->assertContainsString("5\r\nhello\r\n", $buffer);
}
- public function testResponseWithoutExplicitDateHeaderWillAddCurrentDate()
+ public function testResponseWithoutExplicitDateHeaderWillAddCurrentDateFromClock()
{
$server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) {
return new Response();
});
+ $ref = new \ReflectionProperty($server, 'clock');
+ $ref->setAccessible(true);
+ $clock = $ref->getValue($server);
+
+ $ref = new \ReflectionProperty($clock, 'now');
+ $ref->setAccessible(true);
+ $ref->setValue($clock, 1652972091.3958);
+
$buffer = '';
$this->connection
->expects($this->any())
@@ -2318,11 +2339,11 @@ function ($data) use (&$buffer) {
$this->connection->emit('data', array($data));
$this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer);
- $this->assertContainsString("Date:", $buffer);
+ $this->assertContainsString("Date: Thu, 19 May 2022 14:54:51 GMT\r\n", $buffer);
$this->assertContainsString("\r\n\r\n", $buffer);
}
- public function testResponseWIthCustomDateHeaderOverwritesDefault()
+ public function testResponseWithCustomDateHeaderOverwritesDefault()
{
$server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) {
return new Response(
@@ -2491,7 +2512,7 @@ function ($data) use (&$buffer) {
public function testInvalidCallbackFunctionLeadsToException()
{
$this->setExpectedException('InvalidArgumentException');
- $server = new StreamingServer(Loop::get(), 'invalid');
+ new StreamingServer(Loop::get(), 'invalid');
}
public function testResponseBodyStreamWillStreamDataWithChunkedTransferEncoding()
@@ -2906,6 +2927,91 @@ function ($data) use (&$buffer) {
$this->assertInstanceOf('RuntimeException', $exception);
}
+ public static function provideInvalidResponse()
+ {
+ $response = new Response(200, array(), '', '1.1', 'OK');
+
+ return array(
+ array(
+ $response->withStatus(99, 'OK')
+ ),
+ array(
+ $response->withStatus(1000, 'OK')
+ ),
+ array(
+ $response->withStatus(200, "Invald\r\nReason: Yes")
+ ),
+ array(
+ $response->withHeader('Invalid', "Yes\r\n")
+ ),
+ array(
+ $response->withHeader('Invalid', "Yes\n")
+ ),
+ array(
+ $response->withHeader('Invalid', "Yes\r")
+ ),
+ array(
+ $response->withHeader("Inva\r\nlid", 'Yes')
+ ),
+ array(
+ $response->withHeader("Inva\nlid", 'Yes')
+ ),
+ array(
+ $response->withHeader("Inva\rlid", 'Yes')
+ ),
+ array(
+ $response->withHeader('Inva Lid', 'Yes')
+ ),
+ array(
+ $response->withHeader('Inva:Lid', 'Yes')
+ ),
+ array(
+ $response->withHeader('Invalid', "Val\0ue")
+ ),
+ array(
+ $response->withHeader("Inva\0lid", 'Yes')
+ )
+ );
+ }
+
+ /**
+ * @dataProvider provideInvalidResponse
+ * @param ResponseInterface $response
+ */
+ public function testInvalidResponseObjectWillResultInErrorMessage(ResponseInterface $response)
+ {
+ $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($response) {
+ return $response;
+ });
+
+ $exception = null;
+ $server->on('error', function (\Exception $ex) use (&$exception) {
+ $exception = $ex;
+ });
+
+ $buffer = '';
+ $this->connection
+ ->expects($this->any())
+ ->method('write')
+ ->will(
+ $this->returnCallback(
+ function ($data) use (&$buffer) {
+ $buffer .= $data;
+ }
+ )
+ );
+
+ $server->listen($this->socket);
+ $this->socket->emit('connection', array($this->connection));
+
+ $data = $this->createGetRequest();
+
+ $this->connection->emit('data', array($data));
+
+ $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer);
+ $this->assertInstanceOf('InvalidArgumentException', $exception);
+ }
+
public function testRequestServerRequestParams()
{
$requestValidation = null;
@@ -3022,7 +3128,8 @@ public function testRequestCookieWithSeparatorWillBeAddedToServerRequest()
$this->assertEquals(array('hello' => 'world', 'test' => 'abc'), $requestValidation->getCookieParams());
}
- public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() {
+ public function testRequestCookieWithCommaValueWillBeAddedToServerRequest()
+ {
$requestValidation = null;
$server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestValidation) {
$requestValidation = $request;
@@ -3045,7 +3152,7 @@ public function testNewConnectionWillInvokeParserOnce()
{
$server = new StreamingServer(Loop::get(), $this->expectCallableNever());
- $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock();
+ $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock();
$parser->expects($this->once())->method('handle');
$ref = new \ReflectionProperty($server, 'parser');
@@ -3062,7 +3169,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen
$server = new StreamingServer(Loop::get(), $this->expectCallableOnceWith($request));
- $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock();
+ $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock();
$parser->expects($this->once())->method('handle');
$ref = new \ReflectionProperty($server, 'parser');
@@ -3085,7 +3192,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen
$server = new StreamingServer(Loop::get(), $this->expectCallableOnceWith($request));
- $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock();
+ $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock();
$parser->expects($this->once())->method('handle');
$ref = new \ReflectionProperty($server, 'parser');
@@ -3110,7 +3217,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen
return new Response(200, array('Connection' => 'close'));
});
- $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock();
+ $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock();
$parser->expects($this->once())->method('handle');
$ref = new \ReflectionProperty($server, 'parser');
@@ -3135,7 +3242,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle
return new Response();
});
- $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock();
+ $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock();
$parser->expects($this->exactly(2))->method('handle');
$ref = new \ReflectionProperty($server, 'parser');
@@ -3160,7 +3267,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle
return new Response();
});
- $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock();
+ $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock();
$parser->expects($this->exactly(2))->method('handle');
$ref = new \ReflectionProperty($server, 'parser');
@@ -3186,7 +3293,7 @@ public function testNewConnectionWillInvokeParserOnceAfterInvokingRequestHandler
return new Response(200, array(), $body);
});
- $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock();
+ $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock();
$parser->expects($this->once())->method('handle');
$ref = new \ReflectionProperty($server, 'parser');
@@ -3212,7 +3319,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle
return new Response(200, array(), $body);
});
- $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock();
+ $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock();
$parser->expects($this->exactly(2))->method('handle');
$ref = new \ReflectionProperty($server, 'parser');
@@ -3233,6 +3340,25 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle
$this->assertCount(1, $this->connection->listeners('close'));
}
+ public function testCompletingARequestWillRemoveConnectionOnCloseListener()
+ {
+ $connection = $this->mockConnection(array('removeListener'));
+
+ $request = new ServerRequest('GET', 'http://localhost/');
+
+ $server = new StreamingServer(Loop::get(), function () {
+ return \React\Promise\resolve(new Response());
+ });
+
+ $server->listen($this->socket);
+ $this->socket->emit('connection', array($connection));
+
+ $connection->expects($this->once())->method('removeListener');
+
+ // pretend parser just finished parsing
+ $server->handleRequest($connection, $request);
+ }
+
private function createGetRequest()
{
$data = "GET / HTTP/1.1\r\n";
diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php
index d62147b5..284d059f 100644
--- a/tests/Io/TransactionTest.php
+++ b/tests/Io/TransactionTest.php
@@ -2,19 +2,19 @@
namespace React\Tests\Http\Io;
-use Clue\React\Block;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Http\Message\RequestInterface;
-use RingCentral\Psr7\Response;
+use Psr\Http\Message\ResponseInterface;
+use React\Http\Io\ReadableBodyStream;
use React\Http\Io\Transaction;
+use React\Http\Message\Request;
+use React\Http\Message\Response;
use React\Http\Message\ResponseException;
use React\EventLoop\Loop;
use React\Promise;
use React\Promise\Deferred;
use React\Stream\ThroughStream;
use React\Tests\Http\TestCase;
-use RingCentral\Psr7\Request;
-use React\Http\Io\ReadableBodyStream;
class TransactionTest extends TestCase
{
@@ -321,6 +321,8 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenStreamingRe
$stream->close();
$this->assertInstanceOf('React\Promise\PromiseInterface', $promise);
+
+ $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
}
public function testTimeoutExplicitOptionWillRejectWhenTimerFiresAfterStreamingRequestBodyCloses()
@@ -372,13 +374,14 @@ public function testReceivingErrorResponseWillRejectWithResponseException()
$transaction = $transaction->withOptions(array('timeout' => -1));
$promise = $transaction->send($request);
- try {
- Block\await($promise, $loop);
- $this->fail();
- } catch (ResponseException $exception) {
- $this->assertEquals(404, $exception->getCode());
- $this->assertSame($response, $exception->getResponse());
- }
+ $exception = null;
+ $promise->then(null, function ($reason) use (&$exception) {
+ $exception = $reason;
+ });
+
+ assert($exception instanceof ResponseException);
+ $this->assertEquals(404, $exception->getCode());
+ $this->assertSame($response, $exception->getResponse());
}
public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefault()
@@ -399,13 +402,13 @@ public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefau
$transaction = new Transaction($sender, Loop::get());
$promise = $transaction->send($request);
- $response = Block\await($promise);
+ $response = \React\Async\await($promise);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('hello world', (string)$response->getBody());
}
- public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBufferWillRejectAndCloseResponseStream()
+ public function testReceivingStreamingBodyWithContentLengthExceedingMaximumResponseBufferWillRejectAndCloseResponseStreamImmediately()
{
$stream = new ThroughStream();
$stream->on('close', $this->expectCallableOnce());
@@ -419,10 +422,86 @@ public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBuffer
$sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response));
$transaction = new Transaction($sender, Loop::get());
+
$promise = $transaction->send($request);
- $this->setExpectedException('OverflowException');
- Block\await($promise, null, 0.001);
+ $exception = null;
+ $promise->then(null, function ($e) use (&$exception) {
+ $exception = $e;
+ });
+
+ $this->assertFalse($stream->isWritable());
+
+ assert($exception instanceof \OverflowException);
+ $this->assertInstanceOf('OverflowException', $exception);
+ $this->assertEquals('Response body size of 100000000 bytes exceeds maximum of 16777216 bytes', $exception->getMessage());
+ $this->assertEquals(defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90, $exception->getCode());
+ $this->assertNull($exception->getPrevious());
+ }
+
+ public function testReceivingStreamingBodyWithContentsExceedingMaximumResponseBufferWillRejectAndCloseResponseStreamWhenBufferExceedsLimit()
+ {
+ $stream = new ThroughStream();
+ $stream->on('close', $this->expectCallableOnce());
+
+ $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock();
+
+ $response = new Response(200, array(), new ReadableBodyStream($stream));
+
+ // mock sender to resolve promise with the given $response in response to the given $request
+ $sender = $this->makeSenderMock();
+ $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response));
+
+ $transaction = new Transaction($sender, Loop::get());
+ $transaction = $transaction->withOptions(array('maximumSize' => 10));
+ $promise = $transaction->send($request);
+
+ $exception = null;
+ $promise->then(null, function ($e) use (&$exception) {
+ $exception = $e;
+ });
+
+ $this->assertTrue($stream->isWritable());
+ $stream->write('hello wörld');
+ $this->assertFalse($stream->isWritable());
+
+ assert($exception instanceof \OverflowException);
+ $this->assertInstanceOf('OverflowException', $exception);
+ $this->assertEquals('Response body size exceeds maximum of 10 bytes', $exception->getMessage());
+ $this->assertEquals(defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90, $exception->getCode());
+ $this->assertNull($exception->getPrevious());
+ }
+
+ public function testReceivingStreamingBodyWillRejectWhenStreamEmitsError()
+ {
+ $stream = new ThroughStream(function ($data) {
+ throw new \UnexpectedValueException('Unexpected ' . $data, 42);
+ });
+
+ $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock();
+ $response = new Response(200, array(), new ReadableBodyStream($stream));
+
+ // mock sender to resolve promise with the given $response in response to the given $request
+ $sender = $this->makeSenderMock();
+ $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response));
+
+ $transaction = new Transaction($sender, Loop::get());
+ $promise = $transaction->send($request);
+
+ $exception = null;
+ $promise->then(null, function ($e) use (&$exception) {
+ $exception = $e;
+ });
+
+ $this->assertTrue($stream->isWritable());
+ $stream->write('Foo');
+ $this->assertFalse($stream->isWritable());
+
+ assert($exception instanceof \RuntimeException);
+ $this->assertInstanceOf('RuntimeException', $exception);
+ $this->assertEquals('Error while buffering response body: Unexpected Foo', $exception->getMessage());
+ $this->assertEquals(42, $exception->getCode());
+ $this->assertInstanceOf('UnexpectedValueException', $exception->getPrevious());
}
public function testCancelBufferingResponseWillCloseStreamAndReject()
@@ -435,15 +514,26 @@ public function testCancelBufferingResponseWillCloseStreamAndReject()
$response = new Response(200, array(), new ReadableBodyStream($stream));
// mock sender to resolve promise with the given $response in response to the given $request
+ $deferred = new Deferred();
$sender = $this->makeSenderMock();
- $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response));
+ $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise());
$transaction = new Transaction($sender, Loop::get());
$promise = $transaction->send($request);
+
+ $deferred->resolve($response);
$promise->cancel();
- $this->setExpectedException('RuntimeException');
- Block\await($promise, null, 0.001);
+ $exception = null;
+ $promise->then(null, function ($e) use (&$exception) {
+ $exception = $e;
+ });
+
+ assert($exception instanceof \RuntimeException);
+ $this->assertInstanceOf('RuntimeException', $exception);
+ $this->assertEquals('Cancelled buffering response body', $exception->getMessage());
+ $this->assertEquals(0, $exception->getCode());
+ $this->assertNull($exception->getPrevious());
}
public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStreamingIsEnabled()
@@ -461,8 +551,12 @@ public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStre
$transaction = $transaction->withOptions(array('streaming' => true, 'timeout' => -1));
$promise = $transaction->send($request);
- $response = Block\await($promise, $loop);
+ $response = null;
+ $promise->then(function ($value) use (&$response) {
+ $response = $value;
+ });
+ assert($response instanceof ResponseInterface);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('', (string)$response->getBody());
}
@@ -655,7 +749,7 @@ public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting()
array($this->callback(function (RequestInterface $request) use ($that) {
$that->assertFalse($request->hasHeader('Content-Type'));
$that->assertFalse($request->hasHeader('Content-Length'));
- return true;;
+ return true;
}))
)->willReturnOnConsecutiveCalls(
Promise\resolve($redirectResponse),
@@ -666,6 +760,122 @@ public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting()
$transaction->send($requestWithCustomHeaders);
}
+ public function testRequestMethodShouldBeChangedWhenRedirectingWithSeeOther()
+ {
+ $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
+
+ $customHeaders = array(
+ 'Content-Type' => 'text/html; charset=utf-8',
+ 'Content-Length' => '111',
+ );
+
+ $request = new Request('POST', 'http://example.com/', $customHeaders);
+ $sender = $this->makeSenderMock();
+
+ // mock sender to resolve promise with the given $redirectResponse in
+ // response to the given $request
+ $redirectResponse = new Response(303, array('Location' => 'http://example.com/new'));
+
+ // mock sender to resolve promise with the given $okResponse in
+ // response to the given $request
+ $okResponse = new Response(200);
+ $that = $this;
+ $sender->expects($this->exactly(2))->method('send')->withConsecutive(
+ array($this->anything()),
+ array($this->callback(function (RequestInterface $request) use ($that) {
+ $that->assertEquals('GET', $request->getMethod());
+ $that->assertFalse($request->hasHeader('Content-Type'));
+ $that->assertFalse($request->hasHeader('Content-Length'));
+ return true;
+ }))
+ )->willReturnOnConsecutiveCalls(
+ Promise\resolve($redirectResponse),
+ Promise\resolve($okResponse)
+ );
+
+ $transaction = new Transaction($sender, $loop);
+ $transaction->send($request);
+ }
+
+ public function testRequestMethodAndBodyShouldNotBeChangedWhenRedirectingWith307Or308()
+ {
+ $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
+
+ $customHeaders = array(
+ 'Content-Type' => 'text/html; charset=utf-8',
+ 'Content-Length' => '111',
+ );
+
+ $request = new Request('POST', 'http://example.com/', $customHeaders, '{"key":"value"}');
+ $sender = $this->makeSenderMock();
+
+ // mock sender to resolve promise with the given $redirectResponse in
+ // response to the given $request
+ $redirectResponse = new Response(307, array('Location' => 'http://example.com/new'));
+
+ // mock sender to resolve promise with the given $okResponse in
+ // response to the given $request
+ $okResponse = new Response(200);
+ $that = $this;
+ $sender->expects($this->exactly(2))->method('send')->withConsecutive(
+ array($this->anything()),
+ array($this->callback(function (RequestInterface $request) use ($that) {
+ $that->assertEquals('POST', $request->getMethod());
+ $that->assertEquals('{"key":"value"}', (string)$request->getBody());
+ $that->assertEquals(
+ array(
+ 'Content-Type' => array('text/html; charset=utf-8'),
+ 'Content-Length' => array('111'),
+ 'Host' => array('example.com')
+ ),
+ $request->getHeaders()
+ );
+ return true;
+ }))
+ )->willReturnOnConsecutiveCalls(
+ Promise\resolve($redirectResponse),
+ Promise\resolve($okResponse)
+ );
+
+ $transaction = new Transaction($sender, $loop);
+ $transaction->send($request);
+ }
+
+ public function testRedirectingStreamingBodyWith307Or308ShouldThrowCantRedirectStreamException()
+ {
+ $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
+
+ $customHeaders = array(
+ 'Content-Type' => 'text/html; charset=utf-8',
+ 'Content-Length' => '111',
+ );
+
+ $stream = new ThroughStream();
+ $request = new Request('POST', 'http://example.com/', $customHeaders, new ReadableBodyStream($stream));
+ $sender = $this->makeSenderMock();
+
+ // mock sender to resolve promise with the given $redirectResponse in
+ // response to the given $request
+ $redirectResponse = new Response(307, array('Location' => 'http://example.com/new'));
+
+ $sender->expects($this->once())->method('send')->withConsecutive(
+ array($this->anything())
+ )->willReturnOnConsecutiveCalls(
+ Promise\resolve($redirectResponse)
+ );
+
+ $transaction = new Transaction($sender, $loop);
+ $promise = $transaction->send($request);
+
+ $exception = null;
+ $promise->then(null, function ($reason) use (&$exception) {
+ $exception = $reason;
+ });
+
+ assert($exception instanceof \RuntimeException);
+ $this->assertEquals('Unable to redirect request with streaming body', $exception->getMessage());
+ }
+
public function testCancelTransactionWillCancelRequest()
{
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
@@ -773,13 +983,16 @@ public function testCancelTransactionWillCloseBufferingStream()
$body = new ThroughStream();
$body->on('close', $this->expectCallableOnce());
- // mock sender to resolve promise with the given $redirectResponse in
- $redirectResponse = new Response(301, array('Location' => 'http://example.com/new'), new ReadableBodyStream($body));
- $sender->expects($this->once())->method('send')->willReturn(Promise\resolve($redirectResponse));
+ // mock sender to resolve promise with the given $redirectResponse
+ $deferred = new Deferred();
+ $sender->expects($this->once())->method('send')->willReturn($deferred->promise());
$transaction = new Transaction($sender, $loop);
$promise = $transaction->send($request);
+ $redirectResponse = new Response(301, array('Location' => 'http://example.com/new'), new ReadableBodyStream($body));
+ $deferred->resolve($redirectResponse);
+
$promise->cancel();
}
diff --git a/tests/Message/RequestTest.php b/tests/Message/RequestTest.php
new file mode 100644
index 00000000..29baf8a7
--- /dev/null
+++ b/tests/Message/RequestTest.php
@@ -0,0 +1,63 @@
+getBody();
+ $this->assertSame(3, $body->getSize());
+ $this->assertEquals('foo', (string) $body);
+ }
+
+ public function testConstructWithStreamingRequestBodyReturnsBodyWhichImplementsReadableStreamInterfaceWithUnknownSize()
+ {
+ $request = new Request(
+ 'GET',
+ 'http://localhost/',
+ array(),
+ new ThroughStream()
+ );
+
+ $body = $request->getBody();
+ $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body);
+ $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body);
+ $this->assertNull($body->getSize());
+ }
+
+ public function testConstructWithHttpBodyStreamReturnsBodyAsIs()
+ {
+ $request = new Request(
+ 'GET',
+ 'http://localhost/',
+ array(),
+ $body = new HttpBodyStream(new ThroughStream(), 100)
+ );
+
+ $this->assertSame($body, $request->getBody());
+ }
+
+ public function testConstructWithNullBodyThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException', 'Invalid request body given');
+ new Request(
+ 'GET',
+ 'http://localhost/',
+ array(),
+ null
+ );
+ }
+}
diff --git a/tests/Message/ResponseExceptionTest.php b/tests/Message/ResponseExceptionTest.php
index 33eeea9e..b2eaccd3 100644
--- a/tests/Message/ResponseExceptionTest.php
+++ b/tests/Message/ResponseExceptionTest.php
@@ -2,9 +2,9 @@
namespace React\Tests\Http\Message;
+use React\Http\Message\Response;
use React\Http\Message\ResponseException;
use PHPUnit\Framework\TestCase;
-use RingCentral\Psr7\Response;
class ResponseExceptionTest extends TestCase
{
diff --git a/tests/Message/ResponseTest.php b/tests/Message/ResponseTest.php
index ed21cdc2..a9a244c2 100644
--- a/tests/Message/ResponseTest.php
+++ b/tests/Message/ResponseTest.php
@@ -54,6 +54,39 @@ public function testResourceBodyWillThrow()
new Response(200, array(), tmpfile());
}
+ public function testWithStatusReturnsNewInstanceWhenStatusIsChanged()
+ {
+ $response = new Response(200);
+
+ $new = $response->withStatus(404);
+ $this->assertNotSame($response, $new);
+ $this->assertEquals(404, $new->getStatusCode());
+ $this->assertEquals('Not Found', $new->getReasonPhrase());
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('OK', $response->getReasonPhrase());
+ }
+
+ public function testWithStatusReturnsSameInstanceWhenStatusIsUnchanged()
+ {
+ $response = new Response(200);
+
+ $new = $response->withStatus(200);
+ $this->assertSame($response, $new);
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('OK', $response->getReasonPhrase());
+ }
+
+ public function testWithStatusReturnsNewInstanceWhenStatusIsUnchangedButReasonIsChanged()
+ {
+ $response = new Response(200);
+
+ $new = $response->withStatus(200, 'Quite Ok');
+ $this->assertNotSame($response, $new);
+ $this->assertEquals(200, $new->getStatusCode());
+ $this->assertEquals('Quite Ok', $new->getReasonPhrase());
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('OK', $response->getReasonPhrase());
+ }
public function testHtmlMethodReturnsHtmlResponse()
{
@@ -124,4 +157,98 @@ public function testXmlMethodReturnsXmlResponse()
$this->assertEquals('application/xml', $response->getHeaderLine('Content-Type'));
$this->assertEquals('Hello wörld!', (string) $response->getBody());
}
+
+ public function testParseMessageWithMinimalOkResponse()
+ {
+ $response = Response::parseMessage("HTTP/1.1 200 OK\r\n");
+
+ $this->assertEquals('1.1', $response->getProtocolVersion());
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('OK', $response->getReasonPhrase());
+ $this->assertEquals(array(), $response->getHeaders());
+ }
+
+ public function testParseMessageWithSimpleOkResponse()
+ {
+ $response = Response::parseMessage("HTTP/1.1 200 OK\r\nServer: demo\r\n");
+
+ $this->assertEquals('1.1', $response->getProtocolVersion());
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('OK', $response->getReasonPhrase());
+ $this->assertEquals(array('Server' => array('demo')), $response->getHeaders());
+ }
+
+ public function testParseMessageWithSimpleOkResponseWithCustomReasonPhrase()
+ {
+ $response = Response::parseMessage("HTTP/1.1 200 Mostly Okay\r\nServer: demo\r\n");
+
+ $this->assertEquals('1.1', $response->getProtocolVersion());
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('Mostly Okay', $response->getReasonPhrase());
+ $this->assertEquals(array('Server' => array('demo')), $response->getHeaders());
+ }
+
+ public function testParseMessageWithSimpleOkResponseWithEmptyReasonPhraseAppliesDefault()
+ {
+ $response = Response::parseMessage("HTTP/1.1 200 \r\nServer: demo\r\n");
+
+ $this->assertEquals('1.1', $response->getProtocolVersion());
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('OK', $response->getReasonPhrase());
+ $this->assertEquals(array('Server' => array('demo')), $response->getHeaders());
+ }
+
+ public function testParseMessageWithSimpleOkResponseWithoutReasonPhraseAndWhitespaceSeparatorAppliesDefault()
+ {
+ $response = Response::parseMessage("HTTP/1.1 200\r\nServer: demo\r\n");
+
+ $this->assertEquals('1.1', $response->getProtocolVersion());
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('OK', $response->getReasonPhrase());
+ $this->assertEquals(array('Server' => array('demo')), $response->getHeaders());
+ }
+
+ public function testParseMessageWithHttp10SimpleOkResponse()
+ {
+ $response = Response::parseMessage("HTTP/1.0 200 OK\r\nServer: demo\r\n");
+
+ $this->assertEquals('1.0', $response->getProtocolVersion());
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('OK', $response->getReasonPhrase());
+ $this->assertEquals(array('Server' => array('demo')), $response->getHeaders());
+ }
+
+ public function testParseMessageWithHttp10SimpleOkResponseWithLegacyNewlines()
+ {
+ $response = Response::parseMessage("HTTP/1.0 200 OK\nServer: demo\r\n");
+
+ $this->assertEquals('1.0', $response->getProtocolVersion());
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('OK', $response->getReasonPhrase());
+ $this->assertEquals(array('Server' => array('demo')), $response->getHeaders());
+ }
+
+ public function testParseMessageWithInvalidHttpProtocolVersion12Throws()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ Response::parseMessage("HTTP/1.2 200 OK\r\n");
+ }
+
+ public function testParseMessageWithInvalidHttpProtocolVersion2Throws()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ Response::parseMessage("HTTP/2 200 OK\r\n");
+ }
+
+ public function testParseMessageWithInvalidStatusCodeUnderflowThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ Response::parseMessage("HTTP/1.1 99 OK\r\n");
+ }
+
+ public function testParseMessageWithInvalidResponseHeaderFieldThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ Response::parseMessage("HTTP/1.1 200 OK\r\nServer\r\n");
+ }
}
diff --git a/tests/Message/ServerRequestTest.php b/tests/Message/ServerRequestTest.php
index 37cc1879..f82d60f8 100644
--- a/tests/Message/ServerRequestTest.php
+++ b/tests/Message/ServerRequestTest.php
@@ -251,7 +251,7 @@ public function testUrlEncodingForKeyWillReturnValidArray()
);
$cookies = $this->request->getCookieParams();
- $this->assertEquals(array('react;php' => 'is great'), $cookies);
+ $this->assertEquals(array('react%3Bphp' => 'is great'), $cookies);
}
public function testCookieWithoutSpaceAfterSeparatorWillBeAccepted()
@@ -362,4 +362,126 @@ public function testConstructWithResourceRequestBodyThrows()
tmpfile()
);
}
+
+ public function testParseMessageWithSimpleGetRequest()
+ {
+ $request = ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: example.com\r\n", array());
+
+ $this->assertEquals('GET', $request->getMethod());
+ $this->assertEquals('http://example.com/', (string) $request->getUri());
+ $this->assertEquals('1.1', $request->getProtocolVersion());
+ }
+
+ public function testParseMessageWithHttp10RequestWithoutHost()
+ {
+ $request = ServerRequest::parseMessage("GET / HTTP/1.0\r\n", array());
+
+ $this->assertEquals('GET', $request->getMethod());
+ $this->assertEquals('http://127.0.0.1/', (string) $request->getUri());
+ $this->assertEquals('1.0', $request->getProtocolVersion());
+ }
+
+ public function testParseMessageWithOptionsMethodWithAsteriskFormRequestTarget()
+ {
+ $request = ServerRequest::parseMessage("OPTIONS * HTTP/1.1\r\nHost: example.com\r\n", array());
+
+ $this->assertEquals('OPTIONS', $request->getMethod());
+ $this->assertEquals('*', $request->getRequestTarget());
+ $this->assertEquals('1.1', $request->getProtocolVersion());
+ $this->assertEquals('http://example.com/', (string) $request->getUri());
+ }
+
+ public function testParseMessageWithConnectMethodWithAuthorityFormRequestTarget()
+ {
+ $request = ServerRequest::parseMessage("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n", array());
+
+ $this->assertEquals('CONNECT', $request->getMethod());
+ $this->assertEquals('example.com:80', $request->getRequestTarget());
+ $this->assertEquals('1.1', $request->getProtocolVersion());
+ $this->assertEquals('http://example.com/', (string) $request->getUri());
+ }
+
+ public function testParseMessageWithInvalidHttp11RequestWithoutHostThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ ServerRequest::parseMessage("GET / HTTP/1.1\r\n", array());
+ }
+
+ public function testParseMessageWithInvalidHttpProtocolVersionThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ ServerRequest::parseMessage("GET / HTTP/1.2\r\n", array());
+ }
+
+ public function testParseMessageWithInvalidProtocolThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ ServerRequest::parseMessage("GET / CUSTOM/1.1\r\n", array());
+ }
+
+ public function testParseMessageWithInvalidHostHeaderWithoutValueThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost\r\n", array());
+ }
+
+ public function testParseMessageWithInvalidHostHeaderSyntaxThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: ///\r\n", array());
+ }
+
+ public function testParseMessageWithInvalidHostHeaderWithSchemeThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: http://localhost\r\n", array());
+ }
+
+ public function testParseMessageWithInvalidHostHeaderWithQueryThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost?foo\r\n", array());
+ }
+
+ public function testParseMessageWithInvalidHostHeaderWithFragmentThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost#foo\r\n", array());
+ }
+
+ public function testParseMessageWithInvalidContentLengthHeaderThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length:\r\n", array());
+ }
+
+ public function testParseMessageWithInvalidTransferEncodingHeaderThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding:\r\n", array());
+ }
+
+ public function testParseMessageWithInvalidBothContentLengthHeaderAndTransferEncodingHeaderThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\nTransfer-Encoding: chunked\r\n", array());
+ }
+
+ public function testParseMessageWithInvalidEmptyHostHeaderWithAbsoluteFormRequestTargetThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ ServerRequest::parseMessage("GET http://example.com/ HTTP/1.1\r\nHost: \r\n", array());
+ }
+
+ public function testParseMessageWithInvalidConnectMethodNotUsingAuthorityFormThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ ServerRequest::parseMessage("CONNECT / HTTP/1.1\r\nHost: localhost\r\n", array());
+ }
+
+ public function testParseMessageWithInvalidRequestTargetAsteriskFormThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ ServerRequest::parseMessage("GET * HTTP/1.1\r\nHost: localhost\r\n", array());
+ }
}
diff --git a/tests/Message/UriTest.php b/tests/Message/UriTest.php
new file mode 100644
index 00000000..cc4b16f1
--- /dev/null
+++ b/tests/Message/UriTest.php
@@ -0,0 +1,718 @@
+setExpectedException('InvalidArgumentException');
+ new Uri('///');
+ }
+
+ public function testCtorWithInvalidSchemeThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ new Uri('not+a+scheme://localhost');
+ }
+
+ public function testCtorWithInvalidHostThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ new Uri('http://not a host/');
+ }
+
+ public function testCtorWithInvalidPortThrows()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ new Uri('http://localhost:80000/');
+ }
+
+ public static function provideValidUris()
+ {
+ return array(
+ array(
+ 'http://localhost/'
+ ),
+ array(
+ 'http://localhost/'
+ ),
+ array(
+ 'http://localhost:8080/'
+ ),
+ array(
+ 'http://127.0.0.1/'
+ ),
+ array(
+ 'http://[::1]:8080/'
+ ),
+ array(
+ 'http://localhost/path'
+ ),
+ array(
+ 'http://localhost/sub/path'
+ ),
+ array(
+ 'http://localhost/with%20space'
+ ),
+ array(
+ 'http://localhost/with%2fslash'
+ ),
+ array(
+ 'http://localhost/?name=Alice'
+ ),
+ array(
+ 'http://localhost/?name=John+Doe'
+ ),
+ array(
+ 'http://localhost/?name=John%20Doe'
+ ),
+ array(
+ 'http://localhost/?name=Alice&age=42'
+ ),
+ array(
+ 'http://localhost/?name=Alice&'
+ ),
+ array(
+ 'http://localhost/?choice=A%26B'
+ ),
+ array(
+ 'http://localhost/?safe=Yes!?'
+ ),
+ array(
+ 'http://localhost/?alias=@home'
+ ),
+ array(
+ 'http://localhost/?assign:=true'
+ ),
+ array(
+ 'http://localhost/?name='
+ ),
+ array(
+ 'http://localhost/?name'
+ ),
+ array(
+ ''
+ ),
+ array(
+ '/'
+ ),
+ array(
+ '/path'
+ ),
+ array(
+ 'path'
+ ),
+ array(
+ 'http://user@localhost/'
+ ),
+ array(
+ 'http://user@localhost/'
+ ),
+ array(
+ 'http://:pass@localhost/'
+ ),
+ array(
+ 'http://user:pass@localhost/path?query#fragment'
+ ),
+ array(
+ 'http://user%20name:pass%20word@localhost/path%20name?query%20name#frag%20ment'
+ ),
+ array(
+ 'http://docker_container/'
+ )
+ );
+ }
+
+ /**
+ * @dataProvider provideValidUris
+ * @param string $string
+ */
+ public function testToStringReturnsOriginalUriGivenToCtor($string)
+ {
+ if (PHP_VERSION_ID < 50519 || (PHP_VERSION_ID < 50603 && PHP_VERSION_ID >= 50606)) {
+ // @link https://3v4l.org/HdoPG
+ $this->markTestSkipped('Empty password not supported on legacy PHP');
+ }
+
+ $uri = new Uri($string);
+
+ $this->assertEquals($string, (string) $uri);
+ }
+
+ public static function provideValidUrisThatWillBeTransformed()
+ {
+ return array(
+ array(
+ 'http://localhost:8080/?',
+ 'http://localhost:8080/'
+ ),
+ array(
+ 'http://localhost:8080/#',
+ 'http://localhost:8080/'
+ ),
+ array(
+ 'http://localhost:8080/?#',
+ 'http://localhost:8080/'
+ ),
+ array(
+ 'http://localhost:8080/',
+ 'http://localhost:8080/'
+ ),
+ array(
+ 'http://localhost:8080/?percent=50%',
+ 'http://localhost:8080/?percent=50%25'
+ ),
+ array(
+ 'http://user%20name:pass%20word@localhost/path%20name?query%20name#frag%20ment',
+ 'http://user%20name:pass%20word@localhost/path%20name?query%20name#frag%20ment'
+ ),
+ array(
+ 'HTTP://USER:PASS@LOCALHOST:8080/PATH?QUERY#FRAGMENT',
+ 'http://USER:PASS@localhost:8080/PATH?QUERY#FRAGMENT'
+ )
+ );
+ }
+
+ /**
+ * @dataProvider provideValidUrisThatWillBeTransformed
+ * @param string $string
+ * @param string $escaped
+ */
+ public function testToStringReturnsTransformedUriFromUriGivenToCtor($string, $escaped = null)
+ {
+ $uri = new Uri($string);
+
+ $this->assertEquals($escaped, (string) $uri);
+ }
+
+ public function testToStringReturnsUriWithPathPrefixedWithSlashWhenPathDoesNotStartWithSlash()
+ {
+ $uri = new Uri('http://localhost:8080/');
+ $uri = $uri->withPath('path');
+
+ $this->assertEquals('http://localhost:8080/path', (string) $uri);
+ }
+
+ public function testWithSchemeReturnsNewInstanceWhenSchemeIsChanged()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withScheme('https');
+ $this->assertNotSame($uri, $new);
+ $this->assertEquals('https', $new->getScheme());
+ $this->assertEquals('http', $uri->getScheme());
+ }
+
+ public function testWithSchemeReturnsNewInstanceWithSchemeToLowerCaseWhenSchemeIsChangedWithUpperCase()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withScheme('HTTPS');
+ $this->assertNotSame($uri, $new);
+ $this->assertEquals('https', $new->getScheme());
+ $this->assertEquals('http', $uri->getScheme());
+ }
+
+ public function testWithSchemeReturnsNewInstanceWithDefaultPortRemovedWhenSchemeIsChangedToDefaultPortForHttp()
+ {
+ $uri = new Uri('https://localhost:80/');
+
+ $new = $uri->withScheme('http');
+ $this->assertNotSame($uri, $new);
+ $this->assertNull($new->getPort());
+ $this->assertEquals(80, $uri->getPort());
+ }
+
+ public function testWithSchemeReturnsNewInstanceWithDefaultPortRemovedWhenSchemeIsChangedToDefaultPortForHttps()
+ {
+ $uri = new Uri('http://localhost:443/');
+
+ $new = $uri->withScheme('https');
+ $this->assertNotSame($uri, $new);
+ $this->assertNull($new->getPort());
+ $this->assertEquals(443, $uri->getPort());
+ }
+
+ public function testWithSchemeReturnsSameInstanceWhenSchemeIsUnchanged()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withScheme('http');
+ $this->assertSame($uri, $new);
+ $this->assertEquals('http', $uri->getScheme());
+ }
+
+ public function testWithSchemeReturnsSameInstanceWhenSchemeToLowerCaseIsUnchanged()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withScheme('HTTP');
+ $this->assertSame($uri, $new);
+ $this->assertEquals('http', $uri->getScheme());
+ }
+
+ public function testWithSchemeThrowsWhenSchemeIsInvalid()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $this->setExpectedException('InvalidArgumentException');
+ $uri->withScheme('invalid+scheme');
+ }
+
+ public function testWithUserInfoReturnsNewInstanceWhenUserInfoIsChangedWithNameAndPassword()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withUserInfo('user', 'pass');
+ $this->assertNotSame($uri, $new);
+ $this->assertEquals('user:pass', $new->getUserInfo());
+ $this->assertEquals('', $uri->getUserInfo());
+ }
+
+ public function testWithUserInfoReturnsNewInstanceWhenUserInfoIsChangedWithNameOnly()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withUserInfo('user');
+ $this->assertNotSame($uri, $new);
+ $this->assertEquals('user', $new->getUserInfo());
+ $this->assertEquals('', $uri->getUserInfo());
+ }
+
+ public function testWithUserInfoReturnsNewInstanceWhenUserInfoIsChangedWithNameAndEmptyPassword()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withUserInfo('user', '');
+ $this->assertNotSame($uri, $new);
+ $this->assertEquals('user:', $new->getUserInfo());
+ $this->assertEquals('', $uri->getUserInfo());
+ }
+
+ public function testWithUserInfoReturnsNewInstanceWhenUserInfoIsChangedWithPasswordOnly()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withUserInfo('', 'pass');
+ $this->assertNotSame($uri, $new);
+ $this->assertEquals(':pass', $new->getUserInfo());
+ $this->assertEquals('', $uri->getUserInfo());
+ }
+
+ public function testWithUserInfoReturnsNewInstanceWhenUserInfoIsChangedWithNameAndPasswordEncoded()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withUserInfo('user:alice', 'pass%20word');
+ $this->assertNotSame($uri, $new);
+ $this->assertEquals('user%3Aalice:pass%20word', $new->getUserInfo());
+ $this->assertEquals('', $uri->getUserInfo());
+ }
+
+ public function testWithSchemeReturnsSameInstanceWhenSchemeIsUnchangedEmpty()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withUserInfo('');
+ $this->assertSame($uri, $new);
+ $this->assertEquals('', $uri->getUserInfo());
+ }
+
+ public function testWithSchemeReturnsSameInstanceWhenSchemeIsUnchangedWithNameAndPassword()
+ {
+ $uri = new Uri('http://user:pass@localhost/');
+
+ $new = $uri->withUserInfo('user', 'pass');
+ $this->assertSame($uri, $new);
+ $this->assertEquals('user:pass', $uri->getUserInfo());
+ }
+
+ public function testWithHostReturnsNewInstanceWhenHostIsChanged()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withHost('example.com');
+ $this->assertNotSame($uri, $new);
+ $this->assertEquals('example.com', $new->getHost());
+ $this->assertEquals('localhost', $uri->getHost());
+ }
+
+ public function testWithHostReturnsNewInstanceWhenHostIsChangedWithUnderscore()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withHost('docker_container');
+ $this->assertNotSame($uri, $new);
+ $this->assertEquals('docker_container', $new->getHost());
+ $this->assertEquals('localhost', $uri->getHost());
+ }
+
+ public function testWithHostReturnsNewInstanceWithHostToLowerCaseWhenHostIsChangedWithUpperCase()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withHost('EXAMPLE.COM');
+ $this->assertNotSame($uri, $new);
+ $this->assertEquals('example.com', $new->getHost());
+ $this->assertEquals('localhost', $uri->getHost());
+ }
+
+ public function testWithHostReturnsNewInstanceWhenHostIsChangedToEmptyString()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withHost('');
+ $this->assertNotSame($uri, $new);
+ $this->assertEquals('', $new->getHost());
+ $this->assertEquals('localhost', $uri->getHost());
+ }
+
+ public function testWithHostReturnsSameInstanceWhenHostIsUnchanged()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withHost('localhost');
+ $this->assertSame($uri, $new);
+ $this->assertEquals('localhost', $uri->getHost());
+ }
+
+ public function testWithHostReturnsSameInstanceWhenHostToLowerCaseIsUnchanged()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withHost('LOCALHOST');
+ $this->assertSame($uri, $new);
+ $this->assertEquals('localhost', $uri->getHost());
+ }
+
+ public function testWithHostThrowsWhenHostIsInvalidWithPlus()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $this->setExpectedException('InvalidArgumentException');
+ $uri->withHost('invalid+host');
+ }
+
+ public function testWithHostThrowsWhenHostIsInvalidWithSpace()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $this->setExpectedException('InvalidArgumentException');
+ $uri->withHost('invalid host');
+ }
+
+ public function testWithPortReturnsNewInstanceWhenPortIsChanged()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withPort(8080);
+ $this->assertNotSame($uri, $new);
+ $this->assertEquals(8080, $new->getPort());
+ $this->assertNull($uri->getPort());
+ }
+
+ public function testWithPortReturnsNewInstanceWithDefaultPortRemovedWhenPortIsChangedToDefaultPortForHttp()
+ {
+ $uri = new Uri('http://localhost:8080/');
+
+ $new = $uri->withPort(80);
+ $this->assertNotSame($uri, $new);
+ $this->assertNull($new->getPort());
+ $this->assertEquals(8080, $uri->getPort());
+ }
+
+ public function testWithPortReturnsNewInstanceWithDefaultPortRemovedWhenPortIsChangedToDefaultPortForHttps()
+ {
+ $uri = new Uri('https://localhost:8080/');
+
+ $new = $uri->withPort(443);
+ $this->assertNotSame($uri, $new);
+ $this->assertNull($new->getPort());
+ $this->assertEquals(8080, $uri->getPort());
+ }
+
+ public function testWithPortReturnsSameInstanceWhenPortIsUnchanged()
+ {
+ $uri = new Uri('http://localhost:8080/');
+
+ $new = $uri->withPort(8080);
+ $this->assertSame($uri, $new);
+ $this->assertEquals(8080, $uri->getPort());
+ }
+
+ public function testWithPortReturnsSameInstanceWhenPortIsUnchangedDefaultPortForHttp()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withPort(80);
+ $this->assertSame($uri, $new);
+ $this->assertNull($uri->getPort());
+ }
+
+ public function testWithPortReturnsSameInstanceWhenPortIsUnchangedDefaultPortForHttps()
+ {
+ $uri = new Uri('https://localhost/');
+
+ $new = $uri->withPort(443);
+ $this->assertSame($uri, $new);
+ $this->assertNull($uri->getPort());
+ }
+
+ public function testWithPortThrowsWhenPortIsInvalidUnderflow()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $this->setExpectedException('InvalidArgumentException');
+ $uri->withPort(0);
+ }
+
+ public function testWithPortThrowsWhenPortIsInvalidOverflow()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $this->setExpectedException('InvalidArgumentException');
+ $uri->withPort(65536);
+ }
+
+ public function testWithPathReturnsNewInstanceWhenPathIsChanged()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withPath('/path');
+ $this->assertNotSame($uri, $new);
+ $this->assertEquals('/path', $new->getPath());
+ $this->assertEquals('/', $uri->getPath());
+ }
+
+ public function testWithPathReturnsNewInstanceWhenPathIsChangedEncoded()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withPath('/a new/path%20here!');
+ $this->assertNotSame($uri, $new);
+ $this->assertEquals('/a%20new/path%20here!', $new->getPath());
+ $this->assertEquals('/', $uri->getPath());
+ }
+
+ public function testWithPathReturnsSameInstanceWhenPathIsUnchanged()
+ {
+ $uri = new Uri('http://localhost/path');
+
+ $new = $uri->withPath('/path');
+ $this->assertSame($uri, $new);
+ $this->assertEquals('/path', $uri->getPath());
+ }
+
+ public function testWithPathReturnsSameInstanceWhenPathIsUnchangedEncoded()
+ {
+ $uri = new Uri('http://localhost/a%20new/path%20here!');
+
+ $new = $uri->withPath('/a new/path%20here!');
+ $this->assertSame($uri, $new);
+ $this->assertEquals('/a%20new/path%20here!', $uri->getPath());
+ }
+
+ public function testWithQueryReturnsNewInstanceWhenQueryIsChanged()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withQuery('foo=bar');
+ $this->assertNotSame($uri, $new);
+ $this->assertEquals('foo=bar', $new->getQuery());
+ $this->assertEquals('', $uri->getQuery());
+ }
+
+ public function testWithQueryReturnsNewInstanceWhenQueryIsChangedEncoded()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withQuery('foo=a new%20text!');
+ $this->assertNotSame($uri, $new);
+ $this->assertEquals('foo=a%20new%20text!', $new->getQuery());
+ $this->assertEquals('', $uri->getQuery());
+ }
+
+ public function testWithQueryReturnsSameInstanceWhenQueryIsUnchanged()
+ {
+ $uri = new Uri('http://localhost/?foo=bar');
+
+ $new = $uri->withQuery('foo=bar');
+ $this->assertSame($uri, $new);
+ $this->assertEquals('foo=bar', $uri->getQuery());
+ }
+
+ public function testWithQueryReturnsSameInstanceWhenQueryIsUnchangedEncoded()
+ {
+ $uri = new Uri('http://localhost/?foo=a%20new%20text!');
+
+ $new = $uri->withQuery('foo=a new%20text!');
+ $this->assertSame($uri, $new);
+ $this->assertEquals('foo=a%20new%20text!', $uri->getQuery());
+ }
+
+ public function testWithFragmentReturnsNewInstanceWhenFragmentIsChanged()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withFragment('section');
+ $this->assertNotSame($uri, $new);
+ $this->assertEquals('section', $new->getFragment());
+ $this->assertEquals('', $uri->getFragment());
+ }
+
+ public function testWithFragmentReturnsNewInstanceWhenFragmentIsChangedEncoded()
+ {
+ $uri = new Uri('http://localhost/');
+
+ $new = $uri->withFragment('section new%20text!');
+ $this->assertNotSame($uri, $new);
+ $this->assertEquals('section%20new%20text!', $new->getFragment());
+ $this->assertEquals('', $uri->getFragment());
+ }
+
+ public function testWithFragmentReturnsSameInstanceWhenFragmentIsUnchanged()
+ {
+ $uri = new Uri('http://localhost/#section');
+
+ $new = $uri->withFragment('section');
+ $this->assertSame($uri, $new);
+ $this->assertEquals('section', $uri->getFragment());
+ }
+
+ public function testWithFragmentReturnsSameInstanceWhenFragmentIsUnchangedEncoded()
+ {
+ $uri = new Uri('http://localhost/#section%20new%20text!');
+
+ $new = $uri->withFragment('section new%20text!');
+ $this->assertSame($uri, $new);
+ $this->assertEquals('section%20new%20text!', $uri->getFragment());
+ }
+
+ public static function provideResolveUris()
+ {
+ return array(
+ array(
+ 'http://localhost/',
+ '',
+ 'http://localhost/'
+ ),
+ array(
+ 'http://localhost/',
+ 'http://example.com/',
+ 'http://example.com/'
+ ),
+ array(
+ 'http://localhost/',
+ 'path',
+ 'http://localhost/path'
+ ),
+ array(
+ 'http://localhost/',
+ 'path/',
+ 'http://localhost/path/'
+ ),
+ array(
+ 'http://localhost/',
+ 'path//',
+ 'http://localhost/path/'
+ ),
+ array(
+ 'http://localhost/',
+ 'path',
+ 'http://localhost/path'
+ ),
+ array(
+ 'http://localhost/a/b',
+ '/path',
+ 'http://localhost/path'
+ ),
+ array(
+ 'http://localhost/',
+ '/a/b/c',
+ 'http://localhost/a/b/c'
+ ),
+ array(
+ 'http://localhost/a/path',
+ 'b/c',
+ 'http://localhost/a/b/c'
+ ),
+ array(
+ 'http://localhost/a/path',
+ '/b/c',
+ 'http://localhost/b/c'
+ ),
+ array(
+ 'http://localhost/a/path/',
+ 'b/c',
+ 'http://localhost/a/path/b/c'
+ ),
+ array(
+ 'http://localhost/a/path/',
+ '../b/c',
+ 'http://localhost/a/b/c'
+ ),
+ array(
+ 'http://localhost/',
+ '../../../a/b',
+ 'http://localhost/a/b'
+ ),
+ array(
+ 'http://localhost/path',
+ '?query',
+ 'http://localhost/path?query'
+ ),
+ array(
+ 'http://localhost/path',
+ '#fragment',
+ 'http://localhost/path#fragment'
+ ),
+ array(
+ 'http://localhost/path',
+ 'http://localhost/',
+ 'http://localhost/'
+ ),
+ array(
+ 'http://localhost/path',
+ 'http://localhost/?query#fragment',
+ 'http://localhost/?query#fragment'
+ ),
+ array(
+ 'http://localhost/path/?a#fragment',
+ '?b',
+ 'http://localhost/path/?b'
+ ),
+ array(
+ 'http://localhost/path',
+ '//localhost',
+ 'http://localhost/'
+ ),
+ array(
+ 'http://localhost/path',
+ '//localhost/a?query',
+ 'http://localhost/a?query'
+ ),
+ array(
+ 'http://localhost/path',
+ '//LOCALHOST',
+ 'http://localhost/'
+ )
+ );
+ }
+
+ /**
+ * @dataProvider provideResolveUris
+ * @param string $base
+ * @param string $rel
+ * @param string $expected
+ */
+ public function testResolveReturnsResolvedUri($base, $rel, $expected)
+ {
+ $uri = Uri::resolve(new Uri($base), new Uri($rel));
+
+ $this->assertEquals($expected, (string) $uri);
+ }
+}
diff --git a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php
index 7e537391..faf27cb6 100644
--- a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php
+++ b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php
@@ -64,7 +64,10 @@ public function testLimitOneRequestConcurrently()
$this->assertFalse($calledB);
$this->assertFalse($calledC);
- $limitHandlers($requestB, $nextB);
+ $promise = $limitHandlers($requestB, $nextB);
+
+ assert($promise instanceof PromiseInterface);
+ $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
$this->assertTrue($calledA);
$this->assertFalse($calledB);
@@ -79,7 +82,7 @@ public function testLimitOneRequestConcurrently()
/**
* Ensure resolve frees up a slot
*/
- $deferredA->resolve();
+ $deferredA->resolve(null);
$this->assertTrue($calledA);
$this->assertTrue($calledB);
@@ -88,7 +91,7 @@ public function testLimitOneRequestConcurrently()
/**
* Ensure reject also frees up a slot
*/
- $deferredB->reject();
+ $deferredB->reject(new \RuntimeException());
$this->assertTrue($calledA);
$this->assertTrue($calledB);
@@ -188,13 +191,16 @@ public function testStreamDoesPauseAndThenResumeWhenDequeued()
$limitHandlers = new LimitConcurrentRequestsMiddleware(1);
$deferred = new Deferred();
- $limitHandlers(new ServerRequest('GET', 'https://example.com/'), function () use ($deferred) {
+ $promise = $limitHandlers(new ServerRequest('GET', 'https://example.com/'), function () use ($deferred) {
return $deferred->promise();
});
+ assert($promise instanceof PromiseInterface);
+ $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+
$limitHandlers(new ServerRequest('GET', 'https://example.com/', array(), $body), function () {});
- $deferred->reject();
+ $deferred->reject(new \RuntimeException());
}
public function testReceivesBufferedRequestSameInstance()
@@ -283,10 +289,13 @@ public function testReceivesNextRequestAfterPreviousHandlerIsSettled()
$deferred = new Deferred();
$middleware = new LimitConcurrentRequestsMiddleware(1);
- $middleware($request, function () use ($deferred) {
+ $promise = $middleware($request, function () use ($deferred) {
return $deferred->promise();
});
+ assert($promise instanceof PromiseInterface);
+ $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+
$deferred->reject(new \RuntimeException());
$middleware($request, $this->expectCallableOnceWith($request));
@@ -303,10 +312,13 @@ public function testReceivesNextRequestWhichThrowsAfterPreviousHandlerIsSettled(
$deferred = new Deferred();
$middleware = new LimitConcurrentRequestsMiddleware(1);
- $middleware($request, function () use ($deferred) {
+ $promise = $middleware($request, function () use ($deferred) {
return $deferred->promise();
});
+ assert($promise instanceof PromiseInterface);
+ $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+
$second = $middleware($request, function () {
throw new \RuntimeException();
});
@@ -443,16 +455,19 @@ public function testReceivesStreamingBodyChangesInstanceWithCustomBodyButSameDat
$middleware = new LimitConcurrentRequestsMiddleware(1);
$deferred = new Deferred();
- $middleware(new ServerRequest('GET', 'https://example.com/'), function () use ($deferred) {
+ $promise = $middleware(new ServerRequest('GET', 'https://example.com/'), function () use ($deferred) {
return $deferred->promise();
});
+ assert($promise instanceof PromiseInterface);
+ $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+
$req = null;
$middleware($request, function (ServerRequestInterface $request) use (&$req) {
$req = $request;
});
- $deferred->reject();
+ $deferred->reject(new \RuntimeException());
$this->assertNotSame($request, $req);
$this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $req);
@@ -471,10 +486,13 @@ public function testReceivesNextStreamingBodyWithBufferedDataAfterPreviousHandle
{
$deferred = new Deferred();
$middleware = new LimitConcurrentRequestsMiddleware(1);
- $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) {
+ $promise = $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) {
return $deferred->promise();
});
+ assert($promise instanceof PromiseInterface);
+ $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+
$stream = new ThroughStream();
$request = new ServerRequest(
'POST',
@@ -498,10 +516,13 @@ public function testReceivesNextStreamingBodyAndDoesNotEmitDataIfExplicitlyClose
{
$deferred = new Deferred();
$middleware = new LimitConcurrentRequestsMiddleware(1);
- $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) {
+ $promise = $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) {
return $deferred->promise();
});
+ assert($promise instanceof PromiseInterface);
+ $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+
$stream = new ThroughStream();
$request = new ServerRequest(
'POST',
@@ -526,10 +547,13 @@ public function testReceivesNextStreamingBodyAndDoesNotEmitDataIfExplicitlyPause
{
$deferred = new Deferred();
$middleware = new LimitConcurrentRequestsMiddleware(1);
- $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) {
+ $promise = $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) {
return $deferred->promise();
});
+ assert($promise instanceof PromiseInterface);
+ $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+
$stream = new ThroughStream();
$request = new ServerRequest(
'POST',
@@ -554,10 +578,13 @@ public function testReceivesNextStreamingBodyAndDoesEmitDataImmediatelyIfExplici
{
$deferred = new Deferred();
$middleware = new LimitConcurrentRequestsMiddleware(1);
- $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) {
+ $promise = $middleware(new ServerRequest('GET', 'http://example.com/'), function () use ($deferred) {
return $deferred->promise();
});
+ assert($promise instanceof PromiseInterface);
+ $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
+
$stream = new ThroughStream();
$request = new ServerRequest(
'POST',
diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php
index e073e1f0..40c23378 100644
--- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php
+++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php
@@ -2,16 +2,15 @@
namespace React\Tests\Http\Middleware;
-use Clue\React\Block;
use Psr\Http\Message\ServerRequestInterface;
use React\EventLoop\Loop;
+use React\Http\Io\BufferedBody;
use React\Http\Io\HttpBodyStream;
use React\Http\Message\Response;
use React\Http\Message\ServerRequest;
use React\Http\Middleware\RequestBodyBufferMiddleware;
use React\Stream\ThroughStream;
use React\Tests\Http\TestCase;
-use RingCentral\Psr7\BufferStream;
final class RequestBodyBufferMiddlewareTest extends TestCase
{
@@ -46,8 +45,7 @@ public function testAlreadyBufferedResolvesImmediately()
{
$size = 1024;
$body = str_repeat('x', $size);
- $stream = new BufferStream(1024);
- $stream->write($body);
+ $stream = new BufferedBody($body);
$serverRequest = new ServerRequest(
'GET',
'https://example.com/',
@@ -116,10 +114,11 @@ function (ServerRequestInterface $request) use (&$exposedRequest) {
$this->assertSame($body, $exposedRequest->getBody());
}
- public function testKnownExcessiveSizedBodyIsDisgardedTheRequestIsPassedDownToTheNextMiddleware()
+ public function testClosedStreamResolvesImmediatelyWithEmptyBody()
{
$stream = new ThroughStream();
- $stream->end('aa');
+ $stream->close();
+
$serverRequest = new ServerRequest(
'GET',
'https://example.com/',
@@ -127,13 +126,41 @@ public function testKnownExcessiveSizedBodyIsDisgardedTheRequestIsPassedDownToTh
new HttpBodyStream($stream, 2)
);
+ $exposedRequest = null;
$buffer = new RequestBodyBufferMiddleware(1);
- $response = Block\await($buffer(
+ $buffer(
+ $serverRequest,
+ function (ServerRequestInterface $request) use (&$exposedRequest) {
+ $exposedRequest = $request;
+ }
+ );
+
+ $this->assertSame(0, $exposedRequest->getBody()->getSize());
+ $this->assertSame('', $exposedRequest->getBody()->getContents());
+ }
+
+ public function testKnownExcessiveSizedBodyIsDiscardedAndRequestIsPassedDownToTheNextMiddleware()
+ {
+ $stream = new ThroughStream();
+ $serverRequest = new ServerRequest(
+ 'GET',
+ 'https://example.com/',
+ array(),
+ new HttpBodyStream($stream, 2)
+ );
+
+ $buffer = new RequestBodyBufferMiddleware(1);
+
+ $promise = $buffer(
$serverRequest,
function (ServerRequestInterface $request) {
return new Response(200, array(), $request->getBody()->getContents());
}
- ));
+ );
+
+ $stream->end('aa');
+
+ $response = \React\Async\await($promise);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('', $response->getBody()->getContents());
@@ -153,7 +180,7 @@ public function testKnownExcessiveSizedWithIniLikeSize()
);
$buffer = new RequestBodyBufferMiddleware('1K');
- $response = Block\await($buffer(
+ $response = \React\Async\await($buffer(
$serverRequest,
function (ServerRequestInterface $request) {
return new Response(200, array(), $request->getBody()->getContents());
@@ -206,7 +233,7 @@ function (ServerRequestInterface $request) {
$stream->end('aa');
- $exposedResponse = Block\await($promise->then(
+ $exposedResponse = \React\Async\await($promise->then(
null,
$this->expectCallableNever()
));
@@ -215,9 +242,10 @@ function (ServerRequestInterface $request) {
$this->assertSame('', $exposedResponse->getBody()->getContents());
}
- public function testBufferingErrorThrows()
+ public function testBufferingRejectsWhenNextHandlerThrowsWhenStreamEnds()
{
$stream = new ThroughStream();
+
$serverRequest = new ServerRequest(
'GET',
'https://example.com/',
@@ -225,18 +253,101 @@ public function testBufferingErrorThrows()
new HttpBodyStream($stream, null)
);
- $buffer = new RequestBodyBufferMiddleware(1);
+ $buffer = new RequestBodyBufferMiddleware(100);
+ $promise = $buffer(
+ $serverRequest,
+ function (ServerRequestInterface $request) {
+ throw new \RuntimeException('Buffered ' . $request->getBody()->getSize(), 42);
+ }
+ );
+
+ $exception = null;
+ $promise->then(null, function ($e) use (&$exception) {
+ $exception = $e;
+ });
+
+ $this->assertTrue($stream->isWritable());
+ $stream->end('Foo');
+ $this->assertFalse($stream->isWritable());
+
+ assert($exception instanceof \RuntimeException);
+ $this->assertInstanceOf('RuntimeException', $exception);
+ $this->assertEquals('Buffered 3', $exception->getMessage());
+ $this->assertEquals(42, $exception->getCode());
+ $this->assertNull($exception->getPrevious());
+ }
+
+ /**
+ * @requires PHP 7
+ */
+ public function testBufferingRejectsWhenNextHandlerThrowsErrorWhenStreamEnds()
+ {
+ $stream = new ThroughStream();
+
+ $serverRequest = new ServerRequest(
+ 'GET',
+ 'https://example.com/',
+ array(),
+ new HttpBodyStream($stream, null)
+ );
+
+ $buffer = new RequestBodyBufferMiddleware(100);
$promise = $buffer(
$serverRequest,
function (ServerRequestInterface $request) {
- return $request;
+ throw new \Error('Buffered ' . $request->getBody()->getSize(), 42);
}
);
- $stream->emit('error', array(new \RuntimeException()));
+ $exception = null;
+ $promise->then(null, function ($e) use (&$exception) {
+ $exception = $e;
+ });
+
+ $this->assertTrue($stream->isWritable());
+ $stream->end('Foo');
+ $this->assertFalse($stream->isWritable());
- $this->setExpectedException('RuntimeException');
- Block\await($promise);
+ assert($exception instanceof \Error);
+ $this->assertInstanceOf('Error', $exception);
+ $this->assertEquals('Buffered 3', $exception->getMessage());
+ $this->assertEquals(42, $exception->getCode());
+ $this->assertNull($exception->getPrevious());
+ }
+
+ public function testBufferingRejectsWhenStreamEmitsError()
+ {
+ $stream = new ThroughStream(function ($data) {
+ throw new \UnexpectedValueException('Unexpected ' . $data, 42);
+ });
+
+ $serverRequest = new ServerRequest(
+ 'GET',
+ 'https://example.com/',
+ array(),
+ new HttpBodyStream($stream, null)
+ );
+
+ $buffer = new RequestBodyBufferMiddleware(1);
+ $promise = $buffer(
+ $serverRequest,
+ $this->expectCallableNever()
+ );
+
+ $exception = null;
+ $promise->then(null, function ($e) use (&$exception) {
+ $exception = $e;
+ });
+
+ $this->assertTrue($stream->isWritable());
+ $stream->write('Foo');
+ $this->assertFalse($stream->isWritable());
+
+ assert($exception instanceof \RuntimeException);
+ $this->assertInstanceOf('RuntimeException', $exception);
+ $this->assertEquals('Error while buffering request body: Unexpected Foo', $exception->getMessage());
+ $this->assertEquals(42, $exception->getCode());
+ $this->assertInstanceOf('UnexpectedValueException', $exception->getPrevious());
}
public function testFullBodyStreamedBeforeCallingNextMiddleware()
@@ -264,4 +375,35 @@ public function testFullBodyStreamedBeforeCallingNextMiddleware()
$stream->end('aaa');
$this->assertTrue($promiseResolved);
}
+
+ public function testCancelBufferingClosesStreamAndRejectsPromise()
+ {
+ $stream = new ThroughStream();
+ $stream->on('close', $this->expectCallableOnce());
+
+ $serverRequest = new ServerRequest(
+ 'GET',
+ 'https://example.com/',
+ array(),
+ new HttpBodyStream($stream, 2)
+ );
+
+ $buffer = new RequestBodyBufferMiddleware(2);
+
+ $promise = $buffer($serverRequest, $this->expectCallableNever());
+ $promise->cancel();
+
+ $this->assertFalse($stream->isReadable());
+
+ $exception = null;
+ $promise->then(null, function ($e) use (&$exception) {
+ $exception = $e;
+ });
+
+ assert($exception instanceof \RuntimeException);
+ $this->assertInstanceOf('RuntimeException', $exception);
+ $this->assertEquals('Cancelled buffering request body', $exception->getMessage());
+ $this->assertEquals(0, $exception->getCode());
+ $this->assertNull($exception->getPrevious());
+ }
}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 1938ed89..72b7be8d 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -6,7 +6,7 @@
class TestCase extends BaseTestCase
{
- protected function expectCallableOnce()
+ public function expectCallableOnce() // protected (PHP 5.4+)
{
$mock = $this->createCallableMock();
$mock
@@ -16,7 +16,7 @@ protected function expectCallableOnce()
return $mock;
}
- protected function expectCallableOnceWith($value)
+ public function expectCallableOnceWith($value) // protected (PHP 5.4+)
{
$mock = $this->createCallableMock();
$mock
@@ -27,7 +27,7 @@ protected function expectCallableOnceWith($value)
return $mock;
}
- protected function expectCallableNever()
+ public function expectCallableNever() // protected (PHP 5.4+)
{
$mock = $this->createCallableMock();
$mock