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 15db4d7..5a71f99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # 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` diff --git a/README.md b/README.md index 2c98d6c..9ce6381 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,43 @@ -# HttpClient +# Deprecation notice + +This package has now been migrated over to +[react/http](https://github.com/reactphp/http) +and only exists for BC reasons. + +```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) @@ -164,13 +203,13 @@ The recommended way to install this library is [through Composer](https://getcom This will install the latest supported version: ```bash -$ composer require react/http-client:^0.5.7 +$ composer require react/http-client:^0.5.10 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. This project aims to run on any platform and thus does not require any PHP -extensions and supports running on legacy PHP 5.4 through current PHP 7+ and +extensions and supports running on legacy PHP 5.3 through current PHP 7+ and HHVM. It's *highly recommended to use PHP 7+* for this project. diff --git a/composer.json b/composer.json index fe0e2f2..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/promise": "^2.1 || ^1.2.1", "react/socket": "^1.0 || ^0.8.4", "react/stream": "^1.0 || ^0.7.1", - "react/promise": "~2.2", - "evenement/evenement": "^3.0 || ^2.0" + "ringcentral/psr7": "^1.2" }, "require-dev": { - "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" + "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/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 703eee3..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; } @@ -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 88605dd..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; @@ -166,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 62aa84f..83e8858 100644 --- a/tests/DecodeChunkedStreamTest.php +++ b/tests/DecodeChunkedStreamTest.php @@ -10,81 +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"], - ], - 'extra-space-in-length-chunk' => [ - [" 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' => [ - [" \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"), "" - ] - ]; + ) + ); } /** @@ -110,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") - ] - ]; + ) + ); } /** @@ -142,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 cfa6800..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,10 @@ 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 */ @@ -55,11 +106,9 @@ public function testPostDataReturnsData() $data = str_repeat('.', 33000); $request = $client->request('POST', 'https://' . (mt_rand(0, 1) === 0 ? 'eu.' : '') . 'httpbin.org/post', array('Content-Length' => strlen($data))); - $buffer = ''; - $request->on('response', function (Response $response) use (&$buffer) { - $response->on('data', function ($chunk) use (&$buffer) { - $buffer .= $chunk; - }); + $deferred = new Deferred(); + $request->on('response', function (Response $response) use ($deferred) { + $deferred->resolve(Stream\buffer($response)); }); $request->on('error', 'printf'); @@ -67,7 +116,7 @@ public function testPostDataReturnsData() $request->end($data); - $loop->run(); + $buffer = Block\await($deferred->promise(), $loop, self::TIMEOUT_REMOTE); $this->assertNotEquals('', $buffer); @@ -86,11 +135,9 @@ public function testPostJsonReturnsData() $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')); - $buffer = ''; - $request->on('response', function (Response $response) use (&$buffer) { - $response->on('data', function ($chunk) use (&$buffer) { - $buffer .= $chunk; - }); + $deferred = new Deferred(); + $request->on('response', function (Response $response) use ($deferred) { + $deferred->resolve(Stream\buffer($response)); }); $request->on('error', 'printf'); @@ -98,7 +145,7 @@ public function testPostJsonReturnsData() $request->end($data); - $loop->run(); + $buffer = Block\await($deferred->promise(), $loop, self::TIMEOUT_REMOTE); $this->assertNotEquals('', $buffer); @@ -118,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 d7ad6b8..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,9 @@ public function chunkedEncodingResponse() $this->assertSame('Wiki', $buffer); $this->assertSame( - [ + array( 'content-type' => 'text/plain', - ], + ), $response->getHeaders() ); } diff --git a/tests/TestCase.php b/tests/TestCase.php index 9e090bc..901f82f 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -26,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