diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f2f51dd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +/.gitattributes export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore +/examples export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore diff --git a/.travis.yml b/.travis.yml index 05680f2..364429b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,30 @@ language: php -php: - - 5.4 - - 5.5 - - 5.6 - - 7.0 - - 7.1 - - nightly # ignore errors, see below - - hhvm # ignore errors, see below - # lock distro so new future defaults will not break the build dist: trusty matrix: + include: + - php: 5.3 + dist: precise + - php: 5.4 + - php: 5.5 + - php: 5.6 + - php: 7.0 + - php: 7.1 + - php: 7.2 + - php: 7.3 + - php: 7.4 + - php: nightly + - php: hhvm-3.18 + install: + - composer require phpunit/phpunit:^5 --dev --no-interaction # requires legacy phpunit allow_failures: - php: nightly - - php: hhvm + - php: hhvm-3.18 + +install: + - composer install --no-interaction -before_script: - - composer install - script: - vendor/bin/phpunit --coverage-text diff --git a/CHANGELOG.md b/CHANGELOG.md index e4b877e..5a71f99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,68 @@ # Changelog +## 0.5.11 (2021-04-07) + +* Fix: Minimal fix for PHP 8 + (#154 by @remicollet) + +* Documentation: Add deprecation notice to suggest HTTP component instead + (#153 by @clue) + +## 0.5.10 (2020-01-14) + +* Fix: Avoid unneeded warning when decoding invalid data on PHP 7.4. + (#150 by @clue) + +* Add `.gitattributes` to exclude dev files from exports. + (#149 by @reedy) + +* Link to clue/reactphp-buzz for higher-level HTTP client. + (#139 by @clue) + +* Improve test suite by simplifying test matrix and test setup. + (#151 by @clue) + +## 0.5.9 (2018-04-10) + +* Feature: Support legacy HTTP servers that use only `LF` instead of `CRLF`. + (#130 by @clue) + +* Improve test suite by applying maximum test timeouts for integration tests. + (#131 by @clue) + +## 0.5.8 (2018-02-09) + +* Support legacy PHP 5.3 through PHP 7.2 and HHVM + (#126 and #127 by @clue) + +* Improve backwards compatibility with Promise v1 and + use RingCentral to improve interoperability with react/http. + (#124 and #125 by @clue) + +## 0.5.7 (2018-02-08) + +* Fix: Ignore excessive whitespace in chunk header for `Transfer-Encoding: chunked` + (#123 by @DangerLifter and @clue) + +* Fix: Ignore invalid incoming `Transfer-Encoding` response header + (#122 by @clue) + +* Improve documentation for `Client` (and advanced `Connector`) + (#111 by @jsor and #121 by @clue) + +* Improve test suite by adding support for PHPUnit 6 + (#112 by @carusogabriel) + +## 0.5.6 (2017-09-17) + +* Feature: Update Socket component to support HTTP over Unix domain sockets (UDS) + (#110 by @clue) + +## 0.5.5 (2017-09-10) + +* Fix: Update Socket component to work around sending secure HTTPS requests with PHP < 7.1.4 + (#109 by @clue) + ## 0.5.4 (2017-08-25) * Feature: Update Socket dependency to support hosts file on all platforms @@ -8,7 +71,7 @@ This means that HTTP requests to hosts such as `localhost` will now work as expected across all platforms with no changes required: - ``` + ```php $client = new Client($loop); $request = $client->request('GET', 'http://localhost/'); $request->on('response', function (Response $response) { @@ -23,7 +86,7 @@ (#106 by @WyriHaximus) * Improve test suite by locking Travis distro so new defaults will not break the build - (#211 by @clue) + (#105 by @clue) ## 0.5.2 (2017-06-27) diff --git a/README.md b/README.md index 82845b2..9ce6381 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,91 @@ -# HttpClient Component +# Deprecation notice -[![Build Status](https://secure.travis-ci.org/reactphp/http-client.png?branch=master)](http://travis-ci.org/reactphp/http-client) [![Code Climate](https://codeclimate.com/github/reactphp/http-client/badges/gpa.svg)](https://codeclimate.com/github/reactphp/http-client) +This package has now been migrated over to +[react/http](https://github.com/reactphp/http) +and only exists for BC reasons. -Event-driven, streaming HTTP client for [ReactPHP](http://reactphp.org) +```bash +$ composer require react/http +``` + +If you've previously used this package, upgrading may take a moment or two. +The new API has been updated to use Promises and PSR-7 message abstractions. +This means it's now more powerful and easier to use than ever: + +```php +// old +$client = new React\HttpClient\Client($loop); +$request = $client->request('GET', 'https://example.com/'); +$request->on('response', function ($response) { + $response->on('data', function ($chunk) { + echo $chunk; + }); +}); +$request->end(); + +// new +$browser = new React\Http\Browser($loop); +$browser->get('https://example.com/')->then(function (Psr\Http\Message\ResponseInterface $response) { + echo $response->getBody(); +}); +``` + +See [react/http](https://github.com/reactphp/http#client-usage) for more details. + +The below documentation applies to the last release of this package. +Further development will take place in the updated +[react/http](https://github.com/reactphp/http), +so you're highly recommended to upgrade as soon as possible. + +# Deprecated HttpClient + +[![Build Status](https://travis-ci.org/reactphp/http-client.svg?branch=master)](https://travis-ci.org/reactphp/http-client) + +Event-driven, streaming HTTP client for [ReactPHP](https://reactphp.org). **Table of Contents** * [Basic usage](#basic-usage) + * [Client](#client) * [Example](#example) +* [Advanced usage](#advanced-usage) + * [Unix domain sockets](#unix-domain-sockets) * [Install](#install) * [Tests](#tests) * [License](#license) ## Basic usage +### Client + +The `Client` is responsible for communicating with HTTP servers, managing the +connection state and sending your HTTP requests. +It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage). + +```php +$loop = React\EventLoop\Factory::create(); +$client = new Client($loop); +``` + +If you need custom connector settings (DNS resolution, TLS parameters, timeouts, +proxy servers etc.), you can explicitly pass a custom instance of the +[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): + +```php +$connector = new \React\Socket\Connector($loop, array( + 'dns' => '127.0.0.1', + 'tcp' => array( + 'bindto' => '192.168.10.1:0' + ), + 'tls' => array( + 'verify_peer' => false, + 'verify_peer_name' => false + ) +)); + +$client = new Client($loop, $connector); +``` + The `request(string $method, string $uri, array $headers = array(), string $version = '1.0'): Request` method can be used to prepare new Request objects. @@ -98,15 +170,40 @@ $loop->run(); See also the [examples](examples). +## Advanced Usage + +### Unix domain sockets + +By default, this library supports transport over plaintext TCP/IP and secure +TLS connections for the `http://` and `https://` URI schemes respectively. +This library also supports Unix domain sockets (UDS) when explicitly configured. + +In order to use a UDS path, you have to explicitly configure the connector to +override the destination URI so that the hostname given in the request URI will +no longer be used to establish the connection: + +```php +$connector = new FixedUriConnector( + 'unix:///var/run/docker.sock', + new UnixConnector($loop) +); + +$client = new Client($loop, $connector); + +$request = $client->request('GET', 'http://localhost/info'); +``` + +See also [example #11](examples/11-unix-domain-sockets.php). + ## Install -The recommended way to install this library is [through Composer](http://getcomposer.org). -[New to Composer?](http://getcomposer.org/doc/00-intro.md) +The recommended way to install this library is [through Composer](https://getcomposer.org). +[New to Composer?](https://getcomposer.org/doc/00-intro.md) This will install the latest supported version: ```bash -$ composer require react/http-client:^0.5.4 +$ composer require react/http-client:^0.5.10 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. @@ -131,6 +228,15 @@ To run the test suite, go to the project root and run: $ php vendor/bin/phpunit ``` +The test suite also contains a number of functional integration tests that send +test HTTP requests against the online service http://httpbin.org and thus rely +on a stable internet connection. +If you do not want to run these, they can simply be skipped like this: + +```bash +$ php vendor/bin/phpunit --exclude-group internet +``` + ## License MIT, see [LICENSE file](LICENSE). diff --git a/composer.json b/composer.json index aa0e86b..9207639 100644 --- a/composer.json +++ b/composer.json @@ -4,20 +4,27 @@ "keywords": ["http"], "license": "MIT", "require": { - "php": ">=5.4.0", - "guzzlehttp/psr7": "^1.0", + "php": ">=5.3.0", + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3", - "react/socket": "^1.0 || ^0.8.2", - "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.2", - "react/promise": "~2.2", - "evenement/evenement": "^3.0 || ^2.0" + "react/promise": "^2.1 || ^1.2.1", + "react/socket": "^1.0 || ^0.8.4", + "react/stream": "^1.0 || ^0.7.1", + "ringcentral/psr7": "^1.2" }, "require-dev": { - "phpunit/phpunit": "^5.0 || ^4.8.10" + "clue/block-react": "^1.2", + "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35", + "react/promise-stream": "^1.1" }, "autoload": { "psr-4": { "React\\HttpClient\\": "src" } + }, + "autoload-dev": { + "psr-4": { + "React\\Tests\\HttpClient\\": "tests" + } } } diff --git a/examples/11-unix-domain-sockets.php b/examples/11-unix-domain-sockets.php new file mode 100644 index 0000000..ecf22de --- /dev/null +++ b/examples/11-unix-domain-sockets.php @@ -0,0 +1,39 @@ +request('GET', 'http://localhost/info'); + +$request->on('response', function (Response $response) { + var_dump($response->getHeaders()); + + $response->on('data', function ($chunk) { + echo $chunk; + }); + + $response->on('end', function () { + echo 'DONE' . PHP_EOL; + }); +}); + +$request->on('error', function (\Exception $e) { + echo $e; +}); + +$request->end(); + +$loop->run(); diff --git a/phpunit.xml.dist b/phpunit.xml.dist index cba6d4d..04d426b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,8 +8,7 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - syntaxCheck="false" - bootstrap="tests/bootstrap.php" + bootstrap="vendor/autoload.php" > diff --git a/src/ChunkedStreamDecoder.php b/src/ChunkedStreamDecoder.php index 1402077..bc150ad 100644 --- a/src/ChunkedStreamDecoder.php +++ b/src/ChunkedStreamDecoder.php @@ -2,7 +2,7 @@ namespace React\HttpClient; -use Evenement\EventEmitterTrait; +use Evenement\EventEmitter; use Exception; use React\Stream\ReadableStreamInterface; use React\Stream\Util; @@ -11,12 +11,10 @@ /** * @internal */ -class ChunkedStreamDecoder implements ReadableStreamInterface +class ChunkedStreamDecoder extends EventEmitter implements ReadableStreamInterface { const CRLF = "\r\n"; - use EventEmitterTrait; - /** * @var string */ @@ -55,9 +53,9 @@ public function __construct(ReadableStreamInterface $stream) $this->stream = $stream; $this->stream->on('data', array($this, 'handleData')); $this->stream->on('end', array($this, 'handleEnd')); - Util::forwardEvents($this->stream, $this, [ + Util::forwardEvents($this->stream, $this, array( 'error', - ]); + )); } /** @internal */ @@ -89,9 +87,9 @@ protected function iterateBuffer() if ($this->nextChunkIsLength) { $crlfPosition = strpos($this->buffer, static::CRLF); if ($crlfPosition === false && strlen($this->buffer) > 1024) { - $this->emit('error', [ + $this->emit('error', array( new Exception('Chunk length header longer then 1024 bytes'), - ]); + )); $this->close(); return false; } @@ -103,7 +101,7 @@ protected function iterateBuffer() list($lengthChunk) = explode(';', $lengthChunk, 2); } if ($lengthChunk !== '') { - $lengthChunk = ltrim($lengthChunk, "0"); + $lengthChunk = ltrim(trim($lengthChunk), "0"); if ($lengthChunk === '') { // We've reached the end of the stream $this->reachedEnd = true; @@ -113,10 +111,10 @@ protected function iterateBuffer() } } $this->nextChunkIsLength = false; - if (dechex(hexdec($lengthChunk)) !== strtolower($lengthChunk)) { - $this->emit('error', [ + if (dechex((int)@hexdec($lengthChunk)) !== strtolower($lengthChunk)) { + $this->emit('error', array( new Exception('Unable to validate "' . $lengthChunk . '" as chunk length header'), - ]); + )); $this->close(); return false; } @@ -200,9 +198,9 @@ public function handleEnd() $this->emit( 'error', - [ + array( new Exception('Stream ended with incomplete control code') - ] + ) ); $this->close(); } diff --git a/src/Client.php b/src/Client.php index fb8230b..fc14426 100644 --- a/src/Client.php +++ b/src/Client.php @@ -19,7 +19,7 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector = $this->connector = $connector; } - public function request($method, $url, array $headers = [], $protocolVersion = '1.0') + public function request($method, $url, array $headers = array(), $protocolVersion = '1.0') { $requestData = new RequestData($method, $url, $headers, $protocolVersion); diff --git a/src/Request.php b/src/Request.php index 8a23b94..caa242b 100644 --- a/src/Request.php +++ b/src/Request.php @@ -2,12 +2,12 @@ namespace React\HttpClient; -use Evenement\EventEmitterTrait; -use GuzzleHttp\Psr7 as gPsr; +use Evenement\EventEmitter; use React\Promise; +use React\Socket\ConnectionInterface; use React\Socket\ConnectorInterface; use React\Stream\WritableStreamInterface; -use React\Socket\ConnectionInterface; +use RingCentral\Psr7 as gPsr; /** * @event response @@ -15,10 +15,8 @@ * @event error * @event end */ -class Request implements WritableStreamInterface +class Request extends EventEmitter implements WritableStreamInterface { - use EventEmitterTrait; - const STATE_INIT = 0; const STATE_WRITING_HEAD = 1; const STATE_HEAD_WRITTEN = 2; @@ -54,17 +52,18 @@ private function writeHead() $streamRef = &$this->stream; $stateRef = &$this->state; $pendingWrites = &$this->pendingWrites; + $that = $this; $promise = $this->connect(); - $promise->done( - function (ConnectionInterface $stream) use ($requestData, &$streamRef, &$stateRef, &$pendingWrites) { + $promise->then( + function (ConnectionInterface $stream) use ($requestData, &$streamRef, &$stateRef, &$pendingWrites, $that) { $streamRef = $stream; - $stream->on('drain', array($this, 'handleDrain')); - $stream->on('data', array($this, 'handleData')); - $stream->on('end', array($this, 'handleEnd')); - $stream->on('error', array($this, 'handleError')); - $stream->on('close', array($this, 'handleClose')); + $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; @@ -77,7 +76,7 @@ function (ConnectionInterface $stream) use ($requestData, &$streamRef, &$stateRe $pendingWrites = ''; if ($more) { - $this->emit('drain'); + $that->emit('drain'); } } }, @@ -135,7 +134,8 @@ public function handleData($data) { $this->buffer .= $data; - if (false !== strpos($this->buffer, "\r\n\r\n")) { + // 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 { list($response, $bodyChunk) = $this->parseResponse($this->buffer); } catch (\InvalidArgumentException $exception) { @@ -154,11 +154,10 @@ public function handleData($data) return; } - $response->on('close', function () { - $this->close(); - }); - $response->on('error', function (\Exception $error) { - $this->closeError(new \RuntimeException( + $response->on('close', array($this, 'close')); + $that = $this; + $response->on('error', function (\Exception $error) use ($that) { + $that->closeError(new \RuntimeException( "An error occured in the response", 0, $error diff --git a/src/RequestData.php b/src/RequestData.php index 7ec0e2a..1c7d5eb 100644 --- a/src/RequestData.php +++ b/src/RequestData.php @@ -9,7 +9,7 @@ class RequestData private $headers; private $protocolVersion; - public function __construct($method, $url, array $headers = [], $protocolVersion = '1.0') + public function __construct($method, $url, array $headers = array(), $protocolVersion = '1.0') { $this->method = $method; $this->url = $url; diff --git a/src/Response.php b/src/Response.php index 6aa1f12..5ed271f 100644 --- a/src/Response.php +++ b/src/Response.php @@ -12,9 +12,8 @@ * @event error * @event end */ -class Response extends EventEmitter implements ReadableStreamInterface +class Response extends EventEmitter implements ReadableStreamInterface { - private $stream; private $protocol; private $version; @@ -31,17 +30,10 @@ public function __construct(ReadableStreamInterface $stream, $protocol, $version $this->code = $code; $this->reasonPhrase = $reasonPhrase; $this->headers = $headers; - $normalizedHeaders = array_change_key_case($headers, CASE_LOWER); - if (isset($normalizedHeaders['transfer-encoding']) && strtolower($normalizedHeaders['transfer-encoding']) === 'chunked') { + if (strtolower($this->getHeaderLine('Transfer-Encoding')) === 'chunked') { $this->stream = new ChunkedStreamDecoder($stream); - - foreach ($this->headers as $key => $value) { - if (strcasecmp('transfer-encoding', $key) === 0) { - unset($this->headers[$key]); - break; - } - } + $this->removeHeader('Transfer-Encoding'); } $this->stream->on('data', array($this, 'handleData')); @@ -75,6 +67,29 @@ public function getHeaders() return $this->headers; } + private function removeHeader($name) + { + foreach ($this->headers as $key => $value) { + if (strcasecmp($name, $key) === 0) { + unset($this->headers[$key]); + break; + } + } + } + + private function getHeader($name) + { + $name = strtolower($name); + $normalized = array_change_key_case($this->headers, CASE_LOWER); + + return isset($normalized[$name]) ? (array)$normalized[$name] : array(); + } + + private function getHeaderLine($name) + { + return implode(', ' , $this->getHeader($name)); + } + /** @internal */ public function handleData($data) { @@ -150,7 +165,7 @@ public function resume() $this->stream->resume(); } - public function pipe(WritableStreamInterface $dest, array $options = []) + public function pipe(WritableStreamInterface $dest, array $options = array()) { Util::pipe($this, $dest, $options); diff --git a/tests/DecodeChunkedStreamTest.php b/tests/DecodeChunkedStreamTest.php index 435dc00..83e8858 100644 --- a/tests/DecodeChunkedStreamTest.php +++ b/tests/DecodeChunkedStreamTest.php @@ -10,74 +10,81 @@ class DecodeChunkedStreamTest extends TestCase { public function provideChunkedEncoding() { - return [ - 'data-set-1' => [ - ["4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"], - ], - 'data-set-2' => [ - ["4\r\nWiki\r\n", "5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"], - ], - 'data-set-3' => [ - ["4\r\nWiki\r\n", "5\r\n", "pedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"], - ], - 'data-set-4' => [ - ["4\r\nWiki\r\n", "5\r\n", "pedia\r\ne\r\n in\r\n", "\r\nchunks.\r\n0\r\n\r\n"], - ], - 'data-set-5' => [ - ["4\r\n", "Wiki\r\n", "5\r\n", "pedia\r\ne\r\n in\r\n", "\r\nchunks.\r\n0\r\n\r\n"], - ], - 'data-set-6' => [ - ["4\r\n", "Wiki\r\n", "5\r\n", "pedia\r\ne; foo=[bar,beer,pool,cue,win,won]\r\n", " in\r\n", "\r\nchunks.\r\n0\r\n\r\n"], - ], - 'header-fields' => [ - ["4; foo=bar\r\n", "Wiki\r\n", "5\r\n", "pedia\r\ne\r\n", " in\r\n", "\r\nchunks.\r\n", "0\r\n\r\n"], - ], - 'character-for-charactrr' => [ + return array( + 'data-set-1' => array( + array("4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), + ), + 'data-set-2' => array( + array("4\r\nWiki\r\n", "5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), + ), + 'data-set-3' => array( + array("4\r\nWiki\r\n", "5\r\n", "pedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), + ), + 'data-set-4' => array( + array("4\r\nWiki\r\n", "5\r\n", "pedia\r\ne\r\n in\r\n", "\r\nchunks.\r\n0\r\n\r\n"), + ), + 'data-set-5' => array( + array("4\r\n", "Wiki\r\n", "5\r\n", "pedia\r\ne\r\n in\r\n", "\r\nchunks.\r\n0\r\n\r\n"), + ), + 'data-set-6' => array( + array("4\r\n", "Wiki\r\n", "5\r\n", "pedia\r\ne; foo=[bar,beer,pool,cue,win,won]\r\n", " in\r\n", "\r\nchunks.\r\n0\r\n\r\n"), + ), + 'header-fields' => array( + array("4; foo=bar\r\n", "Wiki\r\n", "5\r\n", "pedia\r\ne\r\n", " in\r\n", "\r\nchunks.\r\n", "0\r\n\r\n"), + ), + 'character-for-charactrr' => array( str_split("4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), - ], - 'extra-newline-in-wiki-character-for-chatacter' => [ + ), + 'extra-newline-in-wiki-character-for-chatacter' => array( str_split("6\r\nWi\r\nki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), "Wi\r\nkipedia in\r\n\r\nchunks." - ], - 'extra-newline-in-wiki' => [ - ["6\r\nWi\r\n", "ki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"], + ), + 'extra-newline-in-wiki' => array( + array("6\r\nWi\r\n", "ki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), "Wi\r\nkipedia in\r\n\r\nchunks." - ], - 'varnish-type-response-1' => [ - ["0017\r\nWikipedia in\r\n\r\nchunks.\r\n0\r\n\r\n"] - ], - 'varnish-type-response-2' => [ - ["000017\r\nWikipedia in\r\n\r\nchunks.\r\n0\r\n\r\n"] - ], - 'varnish-type-response-3' => [ - ["017\r\nWikipedia in\r\n\r\nchunks.\r\n0\r\n\r\n"] - ], - 'varnish-type-response-4' => [ - ["004\r\nWiki\r\n005\r\npedia\r\n00e\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"] - ], - 'varnish-type-response-5' => [ - ["000004\r\nWiki\r\n00005\r\npedia\r\n000e\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"] - ], - 'varnish-type-response-extra-line' => [ - ["006\r\nWi\r\nki\r\n005\r\npedia\r\n00e\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"], + ), + 'varnish-type-response-1' => array( + array("0017\r\nWikipedia in\r\n\r\nchunks.\r\n0\r\n\r\n") + ), + 'varnish-type-response-2' => array( + array("000017\r\nWikipedia in\r\n\r\nchunks.\r\n0\r\n\r\n") + ), + 'varnish-type-response-3' => array( + array("017\r\nWikipedia in\r\n\r\nchunks.\r\n0\r\n\r\n") + ), + 'varnish-type-response-4' => array( + array("004\r\nWiki\r\n005\r\npedia\r\n00e\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n") + ), + 'varnish-type-response-5' => array( + array("000004\r\nWiki\r\n00005\r\npedia\r\n000e\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n") + ), + 'varnish-type-response-extra-line' => array( + array("006\r\nWi\r\nki\r\n005\r\npedia\r\n00e\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), "Wi\r\nkipedia in\r\n\r\nchunks." - ], - 'varnish-type-response-random' => [ - [str_repeat("0", rand(0, 10)), "4\r\nWiki\r\n", str_repeat("0", rand(0, 10)), "5\r\npedia\r\n", str_repeat("0", rand(0, 10)), "e\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"] - ], - 'end-chunk-zero-check-1' => [ - ["4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n00\r\n\r\n"] - ], - 'end-chunk-zero-check-2' => [ - ["4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n000\r\n\r\n"] - ], - 'end-chunk-zero-check-3' => [ - ["00004\r\nWiki\r\n005\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0000\r\n\r\n"] - ], - 'uppercase-chunk' => [ - ["4\r\nWiki\r\n5\r\npedia\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"], - ] - ]; + ), + 'varnish-type-response-random' => array( + array(str_repeat("0", rand(0, 10)), "4\r\nWiki\r\n", str_repeat("0", rand(0, 10)), "5\r\npedia\r\n", str_repeat("0", rand(0, 10)), "e\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n") + ), + 'end-chunk-zero-check-1' => array( + array("4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n00\r\n\r\n") + ), + 'end-chunk-zero-check-2' => array( + array("4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n000\r\n\r\n") + ), + 'end-chunk-zero-check-3' => array( + array("00004\r\nWiki\r\n005\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0000\r\n\r\n") + ), + 'uppercase-chunk' => array( + array("4\r\nWiki\r\n5\r\npedia\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), + ), + 'extra-space-in-length-chunk' => array( + array(" 04 \r\nWiki\r\n5\r\npedia\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), + ), + 'only-whitespace-is-final-chunk' => array( + array(" \r\n\r\n"), + "" + ) + ); } /** @@ -103,17 +110,17 @@ public function testChunkedEncoding(array $strings, $expected = "Wikipedia in\r\ public function provideInvalidChunkedEncoding() { - return [ - 'chunk-body-longer-than-header-suggests' => [ - ["4\r\nWiwot40n98w3498tw3049nyn039409t34\r\n", "ki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"], - ], - 'invalid-header-charactrrs' => [ + return array( + 'chunk-body-longer-than-header-suggests' => array( + array("4\r\nWiwot40n98w3498tw3049nyn039409t34\r\n", "ki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"), + ), + 'invalid-header-charactrrs' => array( str_split("xyz\r\nWi\r\nki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n") - ], - 'header-chunk-to-long' => [ + ), + 'header-chunk-to-long' => array( str_split(str_repeat('a', 2015) . "\r\nWi\r\nki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n") - ] - ]; + ) + ); } /** @@ -135,10 +142,10 @@ public function testInvalidChunkedEncoding(array $strings) public function provideZeroChunk() { - return [ - ['1-zero' => "0\r\n\r\n"], - ['random-zero' => str_repeat("0", rand(2, 10))."\r\n\r\n"] - ]; + return array( + array('1-zero' => "0\r\n\r\n"), + array('random-zero' => str_repeat("0", rand(2, 10))."\r\n\r\n") + ); } /** diff --git a/tests/FunctionalIntegrationTest.php b/tests/FunctionalIntegrationTest.php index c59c8a4..cc8d880 100644 --- a/tests/FunctionalIntegrationTest.php +++ b/tests/FunctionalIntegrationTest.php @@ -2,19 +2,43 @@ namespace React\Tests\HttpClient; +use Clue\React\Block; use React\EventLoop\Factory; use React\HttpClient\Client; use React\HttpClient\Response; +use React\Promise\Deferred; +use React\Promise\Stream; use React\Socket\Server; use React\Socket\ConnectionInterface; class FunctionalIntegrationTest extends TestCase { + /** + * Test timeout to use for local tests. + * + * In practice this would be near 0.001s, but let's leave some time in case + * the local system is currently busy. + * + * @var float + */ + const TIMEOUT_LOCAL = 1.0; + + /** + * Test timeout to use for remote (internet) tests. + * + * In pratice this should be below 1s, but this relies on infrastructure + * outside our control, so consider this a maximum to avoid running for hours. + * + * @var float + */ + const TIMEOUT_REMOTE = 10.0; + public function testRequestToLocalhostEmitsSingleRemoteConnection() { $loop = Factory::create(); $server = new Server(0, $loop); + $server->on('connection', $this->expectCallableOnce()); $server->on('connection', function (ConnectionInterface $conn) use ($server) { $conn->end("HTTP/1.1 200 OK\r\n\r\nOk"); $server->close(); @@ -23,9 +47,35 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() $client = new Client($loop); $request = $client->request('GET', 'http://localhost:' . $port); + + $promise = Stream\first($request, 'close'); + $request->end(); + + Block\await($promise, $loop, self::TIMEOUT_LOCAL); + } + + public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResponse() + { + $loop = Factory::create(); + + $server = new Server(0, $loop); + $server->on('connection', function (ConnectionInterface $conn) use ($server) { + $conn->end("HTTP/1.0 200 OK\n\nbody"); + $server->close(); + }); + + $client = new Client($loop); + $request = $client->request('GET', str_replace('tcp:', 'http:', $server->getAddress())); + + $once = $this->expectCallableOnceWith('body'); + $request->on('response', function (Response $response) use ($once) { + $response->on('data', $once); + }); + + $promise = Stream\first($request, 'close'); $request->end(); - $loop->run(); + Block\await($promise, $loop, self::TIMEOUT_LOCAL); } /** @group internet */ @@ -41,9 +91,67 @@ public function testSuccessfulResponseEmitsEnd() $response->on('end', $once); }); + $promise = Stream\first($request, 'close'); $request->end(); - $loop->run(); + Block\await($promise, $loop, self::TIMEOUT_REMOTE); + } + + /** @group internet */ + public function testPostDataReturnsData() + { + $loop = Factory::create(); + $client = new Client($loop); + + $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 (Response $response) use ($deferred) { + $deferred->resolve(Stream\buffer($response)); + }); + + $request->on('error', 'printf'); + $request->on('error', $this->expectCallableNever()); + + $request->end($data); + + $buffer = Block\await($deferred->promise(), $loop, self::TIMEOUT_REMOTE); + + $this->assertNotEquals('', $buffer); + + $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']); + } + + /** @group internet */ + public function testPostJsonReturnsData() + { + $loop = Factory::create(); + $client = new Client($loop); + + $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')); + + $deferred = new Deferred(); + $request->on('response', function (Response $response) use ($deferred) { + $deferred->resolve(Stream\buffer($response)); + }); + + $request->on('error', 'printf'); + $request->on('error', $this->expectCallableNever()); + + $request->end($data); + + $buffer = Block\await($deferred->promise(), $loop, self::TIMEOUT_REMOTE); + + $this->assertNotEquals('', $buffer); + + $parsed = json_decode($buffer, true); + $this->assertTrue(is_array($parsed) && isset($parsed['json'])); + $this->assertEquals(json_decode($data, true), $parsed['json']); } /** @group internet */ @@ -57,7 +165,5 @@ public function testCancelPendingConnectionEmitsClose() $request->on('close', $this->expectCallableOnce()); $request->end(); $request->close(); - - $loop->run(); } } diff --git a/tests/RequestDataTest.php b/tests/RequestDataTest.php index 48ba9be..4db81cd 100644 --- a/tests/RequestDataTest.php +++ b/tests/RequestDataTest.php @@ -126,7 +126,7 @@ public function toStringReturnsHTTPRequestMessageWithHeadersInCustomCase() /** @test */ public function toStringReturnsHTTPRequestMessageWithProtocolVersionThroughConstructor() { - $requestData = new RequestData('GET', 'http://www.example.com', [], '1.1'); + $requestData = new RequestData('GET', 'http://www.example.com', array(), '1.1'); $expected = "GET / HTTP/1.1\r\n" . "Host: www.example.com\r\n" . diff --git a/tests/RequestTest.php b/tests/RequestTest.php index 1d45c9b..0ac5d09 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -272,25 +272,6 @@ public function requestShouldEmitErrorIfUrlHasNoScheme() $request->end(); } - /** - * @test - * @expectedException Exception - * @expectedExceptionMessage something failed - */ - public function requestDoesNotHideErrors() - { - $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); - - $this->rejectedConnectionMock(); - - $request->on('error', function () { - throw new \Exception('something failed'); - }); - - $request->end(); - } - /** @test */ public function postRequestShouldSendAPostRequest() { @@ -653,8 +634,9 @@ private function successfulAsyncConnectionMock() ->with('www.example.com:80') ->will($this->returnValue($deferred->promise())); - return function () use ($deferred) { - $deferred->resolve($this->stream); + $stream = $this->stream; + return function () use ($deferred, $stream) { + $deferred->resolve($stream); }; } @@ -718,7 +700,7 @@ public function chunkedStreamDecoder() $this->stream->expects($this->once()) ->method('emit') - ->with('data', ["1\r\nb\r"]); + ->with('data', array("1\r\nb\r")); $request->handleData("HTTP/1.0 200 OK\r\n"); $request->handleData("Transfer-Encoding: chunked\r\n"); diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 2bea171..a751ab6 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -64,9 +64,9 @@ public function responseShouldEmitEndEventOnEnd() $response->handleEnd(); $this->assertSame( - [ + array( 'Content-Type' => 'text/plain' - ], + ), $response->getHeaders() ); } @@ -89,9 +89,9 @@ public function closedResponseShouldNotBeResumedOrPaused() $response->pause(); $this->assertSame( - [ + array( 'content-type' => 'text/plain', - ], + ), $response->getHeaders() ); } @@ -106,10 +106,10 @@ public function chunkedEncodingResponse() '1.0', '200', 'ok', - [ + array( 'content-type' => 'text/plain', 'transfer-encoding' => 'chunked', - ] + ) ); $buffer = ''; @@ -123,9 +123,40 @@ public function chunkedEncodingResponse() $this->assertSame('Wiki', $buffer); $this->assertSame( - [ + array( 'content-type' => 'text/plain', - ], + ), + $response->getHeaders() + ); + } + + /** @test */ + public function doubleChunkedEncodingResponseWillBePassedAsIs() + { + $stream = new ThroughStream(); + $response = new Response( + $stream, + 'http', + '1.0', + '200', + 'ok', + array( + 'content-type' => 'text/plain', + 'transfer-encoding' => array( + 'chunked', + 'chunked' + ) + ) + ); + + $this->assertSame( + array( + 'content-type' => 'text/plain', + 'transfer-encoding' => array( + 'chunked', + 'chunked' + ) + ), $response->getHeaders() ); } diff --git a/tests/TestCase.php b/tests/TestCase.php index 34cb790..901f82f 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,7 +2,9 @@ namespace React\Tests\HttpClient; -class TestCase extends \PHPUnit_Framework_TestCase +use PHPUnit\Framework\TestCase as BaseTestCase; + +class TestCase extends BaseTestCase { protected function expectCallableExactly($amount) { @@ -24,6 +26,17 @@ protected function expectCallableOnce() return $mock; } + protected function expectCallableOnceWith($value) + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($value); + + return $mock; + } + protected function expectCallableNever() { $mock = $this->createCallableMock(); diff --git a/tests/bootstrap.php b/tests/bootstrap.php deleted file mode 100644 index e3bed44..0000000 --- a/tests/bootstrap.php +++ /dev/null @@ -1,7 +0,0 @@ -addPsr4('React\\Tests\\HttpClient\\', __DIR__); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy