From aeef894447a2f935294003b1433c6db3b9fb2ffb Mon Sep 17 00:00:00 2001 From: Eddie Bimmel Date: Fri, 26 Jun 2020 10:53:23 +0200 Subject: [PATCH 001/152] Greedy matching caused boundary string to contain quotation mark, making it invalid for determining part boundaries --- src/Io/MultipartParser.php | 2 +- tests/Io/MultipartParserTest.php | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 8749b6c5..c9edf43a 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -93,7 +93,7 @@ public function __construct($uploadMaxFilesize = null, $maxFileUploads = null) public function parse(ServerRequestInterface $request) { $contentType = $request->getHeaderLine('content-type'); - if(!\preg_match('/boundary="?(.*)"?$/', $contentType, $matches)) { + if(!\preg_match('/boundary="?(.*?)"?$/', $contentType, $matches)) { return $request; } diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 45ac0c4d..2b883889 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -66,6 +66,39 @@ public function testPostKey() ); } + public function testPostWithQuotationMarkEncapsulatedBoundary() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[one]\"\r\n"; + $data .= "\r\n"; + $data .= "single\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[two]\"\r\n"; + $data .= "\r\n"; + $data .= "second\r\n"; + $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(); + $parsedRequest = $parser->parse($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertSame( + array( + 'users' => array( + 'one' => 'single', + 'two' => 'second', + ), + ), + $parsedRequest->getParsedBody() + ); + } + public function testPostStringOverwritesMap() { $boundary = "---------------------------5844729766471062541057622570"; From f07a923d105051c57e4ada03296bf67f9bb13fb0 Mon Sep 17 00:00:00 2001 From: Eddie Bimmel Date: Fri, 26 Jun 2020 11:14:08 +0200 Subject: [PATCH 002/152] Regex would only work for header parameters that are encapsulate between quotation marks, causing form field names without not to be matched. --- src/Io/MultipartParser.php | 2 +- tests/Io/MultipartParserTest.php | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index c9edf43a..d868ca88 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -278,7 +278,7 @@ private function parseHeaders($header) private function getParameterFromHeader(array $header, $parameter) { foreach ($header as $part) { - if (\preg_match('/' . $parameter . '="?(.*)"$/', $part, $matches)) { + if (\preg_match('/' . $parameter . '="?(.*?)"?$/', $part, $matches)) { return $matches[1]; } } diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 2b883889..91ec832d 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -99,6 +99,39 @@ public function testPostWithQuotationMarkEncapsulatedBoundary() ); } + public function testPostFormDataNamesWithoutQuotationMark() + { + $boundary = "---------------------------5844729766471062541057622570"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=users[one]\r\n"; + $data .= "\r\n"; + $data .= "single\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=users[two]\r\n"; + $data .= "\r\n"; + $data .= "second\r\n"; + $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(); + $parsedRequest = $parser->parse($request); + + $this->assertEmpty($parsedRequest->getUploadedFiles()); + $this->assertSame( + array( + 'users' => array( + 'one' => 'single', + 'two' => 'second', + ), + ), + $parsedRequest->getParsedBody() + ); + } + public function testPostStringOverwritesMap() { $boundary = "---------------------------5844729766471062541057622570"; From 390ca707ecdf097a0039b99bff2210a204ef7063 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sun, 28 Jun 2020 18:23:31 +0200 Subject: [PATCH 003/152] Update for v0.8.6 release --- CHANGELOG.md | 6 ++++++ README.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f0980ce..d1564383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.8.6 (2020-01-12) + +* Fix parsing Cookie request header with comma in its values (#352 by @fiskie) +* Add .gitattributes to exclude dev files from exports (#353 by @reedy) +* Avoid unneeded warning when decoding invalid data on PHP 7.4 (#357 by @WyriHaximus) + ## 0.8.5 (2019-10-29) * Internal refactorings and optimizations to improve request parsing performance. diff --git a/README.md b/README.md index 4fb2febe..36d80d98 100644 --- a/README.md +++ b/README.md @@ -1418,7 +1418,7 @@ The recommended way to install this library is [through Composer](https://getcom This will install the latest supported version: ```bash -$ composer require react/http:^0.8.5 +$ composer require react/http:^0.8.6 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 4d91caecd93c10fb08a77c30854ea820299f3fa0 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Tue, 30 Jun 2020 10:19:24 +0200 Subject: [PATCH 004/152] Run tests on PHPUnit 9 --- composer.json | 2 +- tests/CallableStub.php | 10 -- tests/FunctionalServerTest.php | 52 +++--- tests/Io/ChunkedDecoderTest.php | 6 +- tests/Io/ChunkedEncoderTest.php | 5 +- tests/Io/EmptyBodyStreamTest.php | 29 ++-- tests/Io/HttpBodyStreamTest.php | 29 ++-- tests/Io/IniUtilTest.php | 2 +- tests/Io/LengthLimitedStreamTest.php | 5 +- tests/Io/MiddlewareRunnerTest.php | 13 +- tests/Io/ServerRequestTest.php | 5 +- tests/Io/UploadedFileTest.php | 16 +- .../LimitConcurrentRequestsMiddlewareTest.php | 8 +- .../RequestBodyBufferMiddlewareTest.php | 4 +- tests/ServerTest.php | 9 +- tests/StreamingServerTest.php | 151 +++++++++--------- tests/TestCase.php | 50 +++++- 17 files changed, 205 insertions(+), 191 deletions(-) delete mode 100644 tests/CallableStub.php diff --git a/composer.json b/composer.json index ab26ee88..5330d775 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,6 @@ }, "require-dev": { "clue/block-react": "^1.1", - "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35" } } diff --git a/tests/CallableStub.php b/tests/CallableStub.php deleted file mode 100644 index cfb8acfa..00000000 --- a/tests/CallableStub.php +++ /dev/null @@ -1,10 +0,0 @@ -assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('http://' . noScheme($socket->getAddress()) . '/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -68,7 +68,7 @@ function () { $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 404 Not Found", $response); + $this->assertContainsString("HTTP/1.0 404 Not Found", $response); $socket->close(); } @@ -93,8 +93,8 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('http://' . noScheme($socket->getAddress()) . '/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -119,8 +119,8 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('http://localhost:1000/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('http://localhost:1000/', $response); $socket->close(); } @@ -154,8 +154,8 @@ public function testSecureHttpsOnRandomPort() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('https://' . noScheme($socket->getAddress()) . '/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -194,8 +194,8 @@ public function testSecureHttpsReturnsData() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains("\r\nContent-Length: 33000\r\n", $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString("\r\nContent-Length: 33000\r\n", $response); $this->assertStringEndsWith("\r\n". str_repeat('.', 33000), $response); $socket->close(); @@ -230,8 +230,8 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('https://' . noScheme($socket->getAddress()) . '/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -260,8 +260,8 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('http://127.0.0.1/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('http://127.0.0.1/', $response); $socket->close(); } @@ -290,8 +290,8 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('http://127.0.0.1/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('http://127.0.0.1/', $response); $socket->close(); } @@ -329,8 +329,8 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('https://127.0.0.1/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('https://127.0.0.1/', $response); $socket->close(); } @@ -368,8 +368,8 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('https://127.0.0.1/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('https://127.0.0.1/', $response); $socket->close(); } @@ -398,8 +398,8 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('http://127.0.0.1:443/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('http://127.0.0.1:443/', $response); $socket->close(); } @@ -437,8 +437,8 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() $response = Block\await($result, $loop, 1.0); - $this->assertContains("HTTP/1.0 200 OK", $response); - $this->assertContains('https://127.0.0.1:80/', $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response); + $this->assertContainsString('https://127.0.0.1:80/', $response); $socket->close(); } @@ -784,7 +784,7 @@ function (ServerRequestInterface $request) { $responses = Block\await(Promise\all($result), $loop, 1.0); foreach ($responses as $response) { - $this->assertContains("HTTP/1.0 200 OK", $response, $response); + $this->assertContainsString("HTTP/1.0 200 OK", $response, $response); $this->assertTrue(substr($response, -4) == 1024, $response); } diff --git a/tests/Io/ChunkedDecoderTest.php b/tests/Io/ChunkedDecoderTest.php index 72a98a70..c7889b3f 100644 --- a/tests/Io/ChunkedDecoderTest.php +++ b/tests/Io/ChunkedDecoderTest.php @@ -8,7 +8,11 @@ class ChunkedDecoderTest extends TestCase { - public function setUp() + + /** + * @before + */ + public function setUpParser() { $this->input = new ThroughStream(); $this->parser = new ChunkedDecoder($this->input); diff --git a/tests/Io/ChunkedEncoderTest.php b/tests/Io/ChunkedEncoderTest.php index ef542097..75d43d4a 100644 --- a/tests/Io/ChunkedEncoderTest.php +++ b/tests/Io/ChunkedEncoderTest.php @@ -11,7 +11,10 @@ class ChunkedEncoderTest extends TestCase private $input; private $chunkedStream; - public function setUp() + /** + * @before + */ + public function setUpChunkedStream() { $this->input = new ThroughStream(); $this->chunkedStream = new ChunkedEncoder($this->input); diff --git a/tests/Io/EmptyBodyStreamTest.php b/tests/Io/EmptyBodyStreamTest.php index 6f309fa9..8430239d 100644 --- a/tests/Io/EmptyBodyStreamTest.php +++ b/tests/Io/EmptyBodyStreamTest.php @@ -10,7 +10,10 @@ class EmptyBodyStreamTest extends TestCase private $input; private $bodyStream; - public function setUp() + /** + * @before + */ + public function setUpBodyStream() { $this->bodyStream = new EmptyBodyStream(); } @@ -65,19 +68,15 @@ public function testCloseTwiceEmitsCloseEventAndClearsListeners() $this->assertEquals(array(), $this->bodyStream->listeners('close')); } - /** - * @expectedException BadMethodCallException - */ public function testTell() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->tell(); } - /** - * @expectedException BadMethodCallException - */ public function testEof() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->eof(); } @@ -86,19 +85,15 @@ public function testIsSeekable() $this->assertFalse($this->bodyStream->isSeekable()); } - /** - * @expectedException BadMethodCallException - */ public function testWrite() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->write(''); } - /** - * @expectedException BadMethodCallException - */ public function testRead() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->read(1); } @@ -129,19 +124,15 @@ public function testIsReadableReturnsFalseWhenAlreadyClosed() $this->assertFalse($this->bodyStream->isReadable()); } - /** - * @expectedException BadMethodCallException - */ public function testSeek() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->seek(''); } - /** - * @expectedException BadMethodCallException - */ public function testRewind() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->rewind(); } diff --git a/tests/Io/HttpBodyStreamTest.php b/tests/Io/HttpBodyStreamTest.php index 343a75a5..db21dcf8 100644 --- a/tests/Io/HttpBodyStreamTest.php +++ b/tests/Io/HttpBodyStreamTest.php @@ -11,7 +11,10 @@ class HttpBodyStreamTest extends TestCase private $input; private $bodyStream; - public function setUp() + /** + * @before + */ + public function setUpBodyStream() { $this->input = new ThroughStream(); $this->bodyStream = new HttpBodyStream($this->input, null); @@ -102,19 +105,15 @@ public function testGetSizeCustom() $this->assertEquals(5, $stream->getSize()); } - /** - * @expectedException BadMethodCallException - */ public function testTell() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->tell(); } - /** - * @expectedException BadMethodCallException - */ public function testEof() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->eof(); } @@ -123,19 +122,15 @@ public function testIsSeekable() $this->assertFalse($this->bodyStream->isSeekable()); } - /** - * @expectedException BadMethodCallException - */ public function testWrite() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->write(''); } - /** - * @expectedException BadMethodCallException - */ public function testRead() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->read(''); } @@ -154,19 +149,15 @@ public function testIsReadable() $this->assertTrue($this->bodyStream->isReadable()); } - /** - * @expectedException BadMethodCallException - */ public function testSeek() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->seek(''); } - /** - * @expectedException BadMethodCallException - */ public function testRewind() { + $this->setExpectedException('BadMethodCallException'); $this->bodyStream->rewind(); } diff --git a/tests/Io/IniUtilTest.php b/tests/Io/IniUtilTest.php index 390abab1..80bc422b 100644 --- a/tests/Io/IniUtilTest.php +++ b/tests/Io/IniUtilTest.php @@ -68,10 +68,10 @@ public function provideInvalidInputIniSizeToBytes() /** * @dataProvider provideInvalidInputIniSizeToBytes - * @expectedException InvalidArgumentException */ public function testInvalidInputIniSizeToBytes($input) { + $this->setExpectedException('InvalidArgumentException'); IniUtil::iniSizeToBytes($input); } } diff --git a/tests/Io/LengthLimitedStreamTest.php b/tests/Io/LengthLimitedStreamTest.php index 9a88ba7c..b415269c 100644 --- a/tests/Io/LengthLimitedStreamTest.php +++ b/tests/Io/LengthLimitedStreamTest.php @@ -11,7 +11,10 @@ class LengthLimitedStreamTest extends TestCase private $input; private $stream; - public function setUp() + /** + * @before + */ + public function setUpInput() { $this->input = new ThroughStream(); } diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index 91b927c9..f43231b0 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -18,16 +18,13 @@ final class MiddlewareRunnerTest extends TestCase { - /** - * @expectedException RuntimeException - * @expectedExceptionMessage No middleware to run - */ public function testEmptyMiddlewareStackThrowsException() { $request = new ServerRequest('GET', 'https://example.com/'); $middlewares = array(); $middlewareStack = new MiddlewareRunner($middlewares); + $this->setExpectedException('RuntimeException', 'No middleware to run'); $middlewareStack($request); } @@ -68,10 +65,6 @@ function (ServerRequestInterface $request) use (&$args) { $this->assertEquals(1, $args); } - /** - * @expectedException RuntimeException - * @expectedExceptionMessage hello - */ public function testThrowsIfHandlerThrowsException() { $middleware = new MiddlewareRunner(array( @@ -82,13 +75,12 @@ function (ServerRequestInterface $request) { $request = new ServerRequest('GET', 'http://example.com/'); + $this->setExpectedException('RuntimeException', 'hello'); $middleware($request); } /** * @requires PHP 7 - * @expectedException Throwable - * @expectedExceptionMessage hello */ public function testThrowsIfHandlerThrowsThrowable() { @@ -100,6 +92,7 @@ function (ServerRequestInterface $request) { $request = new ServerRequest('GET', 'http://example.com/'); + $this->setExpectedException('Throwable', 'hello'); $middleware($request); } diff --git a/tests/Io/ServerRequestTest.php b/tests/Io/ServerRequestTest.php index 7a7b241a..47346cd1 100644 --- a/tests/Io/ServerRequestTest.php +++ b/tests/Io/ServerRequestTest.php @@ -9,7 +9,10 @@ class ServerRequestTest extends TestCase { private $request; - public function setUp() + /** + * @before + */ + public function setUpRequest() { $this->request = new ServerRequest('GET', 'http://localhost/'); } diff --git a/tests/Io/UploadedFileTest.php b/tests/Io/UploadedFileTest.php index 383b686b..9ff623da 100644 --- a/tests/Io/UploadedFileTest.php +++ b/tests/Io/UploadedFileTest.php @@ -20,23 +20,21 @@ public function failtyErrorProvider() /** * @dataProvider failtyErrorProvider - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Invalid error code, must be an UPLOAD_ERR_* constant */ public function testFailtyError($error) { $stream = new BufferStream(); + + $this->setExpectedException('InvalidArgumentException', 'Invalid error code, must be an UPLOAD_ERR_* constant'); new UploadedFile($stream, 0, $error, 'foo.bar', 'foo/bar'); } - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage Not implemented - */ public function testNoMoveFile() { $stream = new BufferStream(); $uploadedFile = new UploadedFile($stream, 0, UPLOAD_ERR_OK, 'foo.bar', 'foo/bar'); + + $this->setExpectedException('RuntimeException', 'Not implemented'); $uploadedFile->moveTo('bar.foo'); } @@ -51,14 +49,12 @@ public function testGetters() self::assertSame('foo/bar', $uploadedFile->getClientMediaType()); } - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage Cannot retrieve stream due to upload error - */ public function testGetStreamOnFailedUpload() { $stream = new BufferStream(); $uploadedFile = new UploadedFile($stream, 0, UPLOAD_ERR_NO_FILE, 'foo.bar', 'foo/bar'); + + $this->setExpectedException('RuntimeException', 'Cannot retrieve stream due to upload error'); $uploadedFile->getStream(); } } diff --git a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php index 859b82e7..b4c82390 100644 --- a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php +++ b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php @@ -107,14 +107,11 @@ public function testReturnsResponseDirectlyFromMiddlewareWhenBelowLimit() $this->assertSame($response, $ret); } - /** - * @expectedException RuntimeException - * @expectedExceptionMessage demo - */ public function testThrowsExceptionDirectlyFromMiddlewareWhenBelowLimit() { $middleware = new LimitConcurrentRequestsMiddleware(1); + $this->setExpectedException('RuntimeException', 'demo'); $middleware(new ServerRequest('GET', 'https://example.com/'), function () { throw new \RuntimeException('demo'); }); @@ -122,13 +119,12 @@ public function testThrowsExceptionDirectlyFromMiddlewareWhenBelowLimit() /** * @requires PHP 7 - * @expectedException Error - * @expectedExceptionMessage demo */ public function testThrowsErrorDirectlyFromMiddlewareWhenBelowLimit() { $middleware = new LimitConcurrentRequestsMiddleware(1); + $this->setExpectedException('Error', 'demo'); $middleware(new ServerRequest('GET', 'https://example.com/'), function () { throw new \Error('demo'); }); diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index 28450f6c..98fdfbec 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -221,9 +221,6 @@ function (ServerRequestInterface $request) { $this->assertSame('', $exposedResponse->getBody()->getContents()); } - /** - * @expectedException RuntimeException - */ public function testBufferingErrorThrows() { $loop = Factory::create(); @@ -246,6 +243,7 @@ function (ServerRequestInterface $request) { $stream->emit('error', array(new \RuntimeException())); + $this->setExpectedException('RuntimeException'); Block\await($promise, $loop); } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index e788b8b5..d1d84f59 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -14,7 +14,10 @@ final class ServerTest extends TestCase private $connection; private $socket; - public function setUp() + /** + * @before + */ + public function setUpConnectionMockAndSocket() { $this->connection = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() @@ -40,11 +43,9 @@ public function setUp() $this->socket = new SocketServerStub(); } - /** - * @expectedException InvalidArgumentException - */ public function testInvalidCallbackFunctionLeadsToException() { + $this->setExpectedException('InvalidArgumentException'); new Server('invalid'); } diff --git a/tests/StreamingServerTest.php b/tests/StreamingServerTest.php index 8423e3ea..b620061a 100644 --- a/tests/StreamingServerTest.php +++ b/tests/StreamingServerTest.php @@ -13,7 +13,10 @@ class StreamingServerTest extends TestCase private $connection; private $socket; - public function setUp() + /** + * @before + */ + public function setUpConnectionMockAndSocket() { $this->connection = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() @@ -669,7 +672,7 @@ function ($data) use (&$buffer) { $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); - $this->assertContains("\r\nX-Powered-By: React/alpha\r\n", $buffer); + $this->assertContainsString("\r\nX-Powered-By: React/alpha\r\n", $buffer); } public function testResponsePendingPromiseWillNotSendAnything() @@ -1156,8 +1159,8 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContains("bye", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("bye", $buffer); } public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp10() @@ -1189,9 +1192,9 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.0\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.0 200 OK\r\n", $buffer); - $this->assertContains("\r\n\r\n", $buffer); - $this->assertContains("bye", $buffer); + $this->assertContainsString("HTTP/1.0 200 OK\r\n", $buffer); + $this->assertContainsString("\r\n\r\n", $buffer); + $this->assertContainsString("bye", $buffer); } public function testResponseContainsNoResponseBodyForHeadRequest() @@ -1222,8 +1225,8 @@ function ($data) use (&$buffer) { $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertNotContains("bye", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertNotContainsString("bye", $buffer); } public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContentStatus() @@ -1254,9 +1257,9 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 204 No Content\r\n", $buffer); - $this->assertNotContains("\r\n\Content-Length: 3\r\n", $buffer); - $this->assertNotContains("bye", $buffer); + $this->assertContainsString("HTTP/1.1 204 No Content\r\n", $buffer); + $this->assertNotContainsString("\r\n\Content-Length: 3\r\n", $buffer); + $this->assertNotContainsString("bye", $buffer); } public function testResponseContainsNoResponseBodyForNotModifiedStatus() @@ -1287,9 +1290,9 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 304 Not Modified\r\n", $buffer); - $this->assertContains("\r\nContent-Length: 3\r\n", $buffer); - $this->assertNotContains("bye", $buffer); + $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); + $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); + $this->assertNotContainsString("bye", $buffer); } public function testRequestInvalidHttpProtocolVersionWillEmitErrorAndSendErrorResponse() @@ -1321,9 +1324,9 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('InvalidArgumentException', $error); - $this->assertContains("HTTP/1.1 505 HTTP Version not supported\r\n", $buffer); - $this->assertContains("\r\n\r\n", $buffer); - $this->assertContains("Error 505: HTTP Version not supported", $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); } public function testRequestOverflowWillEmitErrorAndSendErrorResponse() @@ -1356,8 +1359,8 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('OverflowException', $error); - $this->assertContains("HTTP/1.1 431 Request Header Fields Too Large\r\n", $buffer); - $this->assertContains("\r\n\r\nError 431: Request Header Fields Too Large", $buffer); + $this->assertContainsString("HTTP/1.1 431 Request Header Fields Too Large\r\n", $buffer); + $this->assertContainsString("\r\n\r\nError 431: Request Header Fields Too Large", $buffer); } public function testRequestInvalidWillEmitErrorAndSendErrorResponse() @@ -1389,8 +1392,8 @@ function ($data) use (&$buffer) { $this->assertInstanceOf('InvalidArgumentException', $error); - $this->assertContains("HTTP/1.1 400 Bad Request\r\n", $buffer); - $this->assertContains("\r\n\r\nError 400: Bad Request", $buffer); + $this->assertContainsString("HTTP/1.1 400 Bad Request\r\n", $buffer); + $this->assertContainsString("\r\n\r\nError 400: Bad Request", $buffer); } public function testRequestContentLengthBodyDataWillEmitDataEventOnRequestStream() @@ -1905,8 +1908,8 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); $stream->emit('data', array('hello')); - $this->assertContains("Transfer-Encoding: chunked", $buffer); - $this->assertContains("hello", $buffer); + $this->assertContainsString("Transfer-Encoding: chunked", $buffer); + $this->assertContainsString("hello", $buffer); } public function testResponseWithBodyStringWillOverwriteExplicitContentLengthAndTransferEncoding() @@ -1941,9 +1944,9 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertNotContains("Transfer-Encoding: chunked", $buffer); - $this->assertContains("Content-Length: 5", $buffer); - $this->assertContains("hello", $buffer); + $this->assertNotContainsString("Transfer-Encoding: chunked", $buffer); + $this->assertContainsString("Content-Length: 5", $buffer); + $this->assertContainsString("hello", $buffer); } public function testResponseContainsResponseBodyWithTransferEncodingChunkedForBodyWithUnknownSize() @@ -1978,9 +1981,9 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertContains("Transfer-Encoding: chunked", $buffer); - $this->assertNotContains("Content-Length:", $buffer); - $this->assertContains("body", $buffer); + $this->assertContainsString("Transfer-Encoding: chunked", $buffer); + $this->assertNotContainsString("Content-Length:", $buffer); + $this->assertContainsString("body", $buffer); } public function testResponseContainsResponseBodyWithPlainBodyWithUnknownSizeForLegacyHttp10() @@ -2015,9 +2018,9 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertNotContains("Transfer-Encoding: chunked", $buffer); - $this->assertNotContains("Content-Length:", $buffer); - $this->assertContains("body", $buffer); + $this->assertNotContainsString("Transfer-Encoding: chunked", $buffer); + $this->assertNotContainsString("Content-Length:", $buffer); + $this->assertContainsString("body", $buffer); } public function testResponseWithCustomTransferEncodingWillBeIgnoredAndUseChunkedTransferEncodingInstead() @@ -2053,9 +2056,9 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); $stream->emit('data', array('hello')); - $this->assertContains('Transfer-Encoding: chunked', $buffer); - $this->assertNotContains('Transfer-Encoding: custom', $buffer); - $this->assertContains("5\r\nhello\r\n", $buffer); + $this->assertContainsString('Transfer-Encoding: chunked', $buffer); + $this->assertNotContainsString('Transfer-Encoding: custom', $buffer); + $this->assertContainsString("5\r\nhello\r\n", $buffer); } public function testResponseWithoutExplicitDateHeaderWillAddCurrentDate() @@ -2083,9 +2086,9 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContains("Date:", $buffer); - $this->assertContains("\r\n\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("Date:", $buffer); + $this->assertContainsString("\r\n\r\n", $buffer); } public function testResponseWIthCustomDateHeaderOverwritesDefault() @@ -2116,9 +2119,9 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContains("Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", $buffer); - $this->assertContains("\r\n\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", $buffer); + $this->assertContainsString("\r\n\r\n", $buffer); } public function testResponseWithEmptyDateHeaderRemovesDateHeader() @@ -2149,9 +2152,9 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertNotContains("Date:", $buffer); - $this->assertContains("\r\n\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertNotContainsString("Date:", $buffer); + $this->assertContainsString("\r\n\r\n", $buffer); } public function testResponseCanContainMultipleCookieHeaders() @@ -2220,8 +2223,8 @@ function ($data) use (&$buffer) { $data .= "\r\n"; $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 100 Continue\r\n", $buffer); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 100 Continue\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); } public function testResponseWithExpectContinueRequestWontSendContinueForHttp10() @@ -2250,15 +2253,13 @@ function ($data) use (&$buffer) { $data .= "\r\n"; $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.0 200 OK\r\n", $buffer); - $this->assertNotContains("HTTP/1.1 100 Continue\r\n\r\n", $buffer); + $this->assertContainsString("HTTP/1.0 200 OK\r\n", $buffer); + $this->assertNotContainsString("HTTP/1.1 100 Continue\r\n\r\n", $buffer); } - /** - * @expectedException InvalidArgumentException - */ public function testInvalidCallbackFunctionLeadsToException() { + $this->setExpectedException('InvalidArgumentException'); $server = new StreamingServer('invalid'); } @@ -2295,10 +2296,10 @@ function ($data) use (&$buffer) { $input->emit('data', array('1')); $input->emit('data', array('23')); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContains("\r\n\r\n", $buffer); - $this->assertContains("1\r\n1\r\n", $buffer); - $this->assertContains("2\r\n23\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("\r\n\r\n", $buffer); + $this->assertContainsString("1\r\n1\r\n", $buffer); + $this->assertContainsString("2\r\n23\r\n", $buffer); } public function testResponseBodyStreamWithContentLengthWillStreamTillLengthWithoutTransferEncoding() @@ -2334,11 +2335,11 @@ function ($data) use (&$buffer) { $input->emit('data', array('hel')); $input->emit('data', array('lo')); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContains("Content-Length: 5\r\n", $buffer); - $this->assertNotContains("Transfer-Encoding", $buffer); - $this->assertContains("\r\n\r\n", $buffer); - $this->assertContains("hello", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("Content-Length: 5\r\n", $buffer); + $this->assertNotContainsString("Transfer-Encoding", $buffer); + $this->assertContainsString("\r\n\r\n", $buffer); + $this->assertContainsString("hello", $buffer); } public function testResponseWithResponsePromise() @@ -2365,8 +2366,8 @@ function ($data) use (&$buffer) { $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContains("\r\n\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("\r\n\r\n", $buffer); } public function testResponseReturnInvalidTypeWillResultInError() @@ -2399,7 +2400,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); $this->assertInstanceOf('RuntimeException', $exception); } @@ -2428,7 +2429,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } public function testResponseRejectedPromiseWillResultInErrorMessage() @@ -2459,7 +2460,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } public function testResponseExceptionInCallbackWillResultInErrorMessage() @@ -2490,7 +2491,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } public function testResponseWithContentLengthHeaderForStringBodyOverwritesTransferEncoding() @@ -2522,11 +2523,11 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContains("Content-Length: 5\r\n", $buffer); - $this->assertContains("hello", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("Content-Length: 5\r\n", $buffer); + $this->assertContainsString("hello", $buffer); - $this->assertNotContains("Transfer-Encoding", $buffer); + $this->assertNotContainsString("Transfer-Encoding", $buffer); } public function testResponseWillBeHandled() @@ -2554,7 +2555,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); } public function testResponseExceptionThrowInCallBackFunctionWillResultInErrorMessage() @@ -2588,7 +2589,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); $this->assertInstanceOf('RuntimeException', $exception); - $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); $this->assertEquals('hello', $exception->getPrevious()->getMessage()); } @@ -2634,7 +2635,7 @@ function ($data) use (&$buffer) { } $this->assertInstanceOf('RuntimeException', $exception); - $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); $this->assertEquals('hello', $exception->getPrevious()->getMessage()); } @@ -2670,7 +2671,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); - $this->assertContains("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); $this->assertInstanceOf('RuntimeException', $exception); } diff --git a/tests/TestCase.php b/tests/TestCase.php index 2b6d265d..6295e871 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -63,8 +63,52 @@ protected function expectCallableConsecutive($numberOfCalls, array $with) protected function createCallableMock() { - return $this - ->getMockBuilder('React\Tests\Http\CallableStub') - ->getMock(); + if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { + // PHPUnit 10+ + return $this->getMockBuilder('stdClass')->addMethods(array('__invoke'))->getMock(); + } else { + // legacy PHPUnit 4 - PHPUnit 9 + return $this->getMockBuilder('stdClass')->setMethods(array('__invoke'))->getMock(); + } + } + + public function assertContainsString($needle, $haystack) + { + if (method_exists($this, 'assertStringContainsString')) { + // PHPUnit 7.5+ + $this->assertStringContainsString($needle, $haystack); + } else { + // legacy PHPUnit 4 - PHPUnit 7.5 + $this->assertContains($needle, $haystack); + } + } + + public function assertNotContainsString($needle, $haystack) + { + if (method_exists($this, 'assertStringNotContainsString')) { + // PHPUnit 7.5+ + $this->assertStringNotContainsString($needle, $haystack); + } else { + // legacy PHPUnit 4 - PHPUnit 7.5 + $this->assertNotContains($needle, $haystack); + } } + + public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null) + { + if (method_exists($this, 'expectException')) { + // PHPUnit 5+ + $this->expectException($exception); + if ($exceptionMessage !== '') { + $this->expectExceptionMessage($exceptionMessage); + } + if ($exceptionCode !== null) { + $this->expectExceptionCode($exceptionCode); + } + } else { + // legacy PHPUnit 4 + parent::setExpectedException($exception, $exceptionMessage, $exceptionCode); + } + } + } From 087c420163da1b1bfd433fcc645ecc869bdd9887 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Tue, 30 Jun 2020 10:31:43 +0200 Subject: [PATCH 005/152] Clean up test suite --- .travis.yml | 4 ++-- composer.json | 11 ++++++----- phpunit.xml.dist | 11 +---------- tests/FunctionalServerTest.php | 12 ++++++------ tests/Io/RequestHeaderParserTest.php | 4 ++++ tests/bootstrap.php | 7 ------- 6 files changed, 19 insertions(+), 30 deletions(-) delete mode 100644 tests/bootstrap.php diff --git a/.travis.yml b/.travis.yml index 59b46fbd..d3de1e55 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,9 +18,9 @@ matrix: - php: 7.2 - php: 7.3 - php: 7.4 - - php: hhvm + - php: hhvm-3.18 allow_failures: - - php: hhvm + - php: hhvm-3.18 install: - composer install --no-interaction diff --git a/composer.json b/composer.json index 5330d775..d750445a 100644 --- a/composer.json +++ b/composer.json @@ -12,13 +12,14 @@ "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "react/promise-stream": "^1.1" }, - "autoload": { - "psr-4": { - "React\\Http\\": "src" - } - }, "require-dev": { "clue/block-react": "^1.1", "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35" + }, + "autoload": { + "psr-4": { "React\\Http\\": "src" } + }, + "autoload-dev": { + "psr-4": { "React\\Tests\\Http\\": "tests" } } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 79c0ee66..0e947b87 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,15 +1,6 @@ - + ./tests/ diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index 187c541d..e0feb476 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -127,8 +127,8 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() public function testSecureHttpsOnRandomPort() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); } $loop = Factory::create(); @@ -162,8 +162,8 @@ public function testSecureHttpsOnRandomPort() public function testSecureHttpsReturnsData() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); } $loop = Factory::create(); @@ -203,8 +203,8 @@ public function testSecureHttpsReturnsData() public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); } $loop = Factory::create(); diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index baf7215a..ca18df13 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -715,6 +715,10 @@ public function testServerParamsWillNotSetRemoteAddressForUnixDomainSockets() public function testServerParamsWontBeSetOnMissingUrls() { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); + } + $request = null; $parser = new RequestHeaderParser(); diff --git a/tests/bootstrap.php b/tests/bootstrap.php deleted file mode 100644 index cc300c15..00000000 --- a/tests/bootstrap.php +++ /dev/null @@ -1,7 +0,0 @@ -addPsr4('React\\Tests\\Http\\', __DIR__); From 19b1230b117fd8ee58c1acd09390fcd72e70f2f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 4 Jul 2020 11:37:55 +0200 Subject: [PATCH 006/152] Fix calculating concurrency when `post_max_size` ini is unlimited --- src/Server.php | 19 +++++++++++------ tests/Io/IniUtilTest.php | 5 +++++ tests/ServerTest.php | 44 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/Server.php b/src/Server.php index 76428aaa..cf3f9cac 100644 --- a/src/Server.php +++ b/src/Server.php @@ -127,7 +127,9 @@ public function __construct($requestHandler) } $middleware = array(); - $middleware[] = new LimitConcurrentRequestsMiddleware($this->getConcurrentRequestsLimit()); + $middleware[] = new LimitConcurrentRequestsMiddleware( + $this->getConcurrentRequestsLimit(\ini_get('memory_limit'), \ini_get('post_max_size')) + ); $middleware[] = new RequestBodyBufferMiddleware(); // Checking for an empty string because that is what a boolean // false is returned as by ini_get depending on the PHP version. @@ -162,17 +164,22 @@ public function listen(ServerInterface $server) } /** + * @param string $memory_limit + * @param string $post_max_size * @return int - * @codeCoverageIgnore */ - private function getConcurrentRequestsLimit() + private function getConcurrentRequestsLimit($memory_limit, $post_max_size) { - if (\ini_get('memory_limit') == -1) { + if ($memory_limit == -1) { return self::MAXIMUM_CONCURRENT_REQUESTS; } - $availableMemory = IniUtil::iniSizeToBytes(\ini_get('memory_limit')) / 4; - $concurrentRequests = \ceil($availableMemory / IniUtil::iniSizeToBytes(\ini_get('post_max_size'))); + if ($post_max_size == 0) { + return 1; + } + + $availableMemory = IniUtil::iniSizeToBytes($memory_limit) / 4; + $concurrentRequests = (int) \ceil($availableMemory / IniUtil::iniSizeToBytes($post_max_size)); if ($concurrentRequests >= self::MAXIMUM_CONCURRENT_REQUESTS) { return self::MAXIMUM_CONCURRENT_REQUESTS; diff --git a/tests/Io/IniUtilTest.php b/tests/Io/IniUtilTest.php index 80bc422b..22374eb4 100644 --- a/tests/Io/IniUtilTest.php +++ b/tests/Io/IniUtilTest.php @@ -53,6 +53,11 @@ public function testIniSizeToBytes($input, $output) $this->assertEquals($output, IniUtil::iniSizeToBytes($input)); } + public function testIniSizeToBytesWithInvalidSuffixReturnsNumberWithoutSuffix() + { + $this->assertEquals('2', IniUtil::iniSizeToBytes('2x')); + } + public function provideInvalidInputIniSizeToBytes() { return array( diff --git a/tests/ServerTest.php b/tests/ServerTest.php index d1d84f59..4ca81e16 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -183,4 +183,48 @@ private function createPostFileUploadRequest() return $data; } + + public function provideIniSettingsForConcurrency() + { + return array( + 'default settings' => array( + '128M', + '8M', + 4 + ), + 'unlimited memory_limit limited to maximum concurrency' => array( + '-1', + '8M', + 100 + ), + 'unlimited post_max_size' => array( + '128M', + '0', + 1 + ), + 'small post_max_size limited to maximum concurrency' => array( + '128M', + '1k', + 100 + ) + ); + } + + /** + * @param string $memory_limit + * @param string $post_max_size + * @param int $expectedConcurrency + * @dataProvider provideIniSettingsForConcurrency + */ + public function testServerConcurrency($memory_limit, $post_max_size, $expectedConcurrency) + { + $server = new Server(function () { }); + + $ref = new \ReflectionMethod($server, 'getConcurrentRequestsLimit'); + $ref->setAccessible(true); + + $value = $ref->invoke($server, $memory_limit, $post_max_size); + + $this->assertEquals($expectedConcurrency, $value); + } } From 9aa446fd86745403a09c315c90d4e464fd84382b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 5 Jul 2020 13:32:28 +0200 Subject: [PATCH 007/152] Prepare v0.8.7 release --- CHANGELOG.md | 22 +++++++++++++++++++--- README.md | 2 +- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1564383..465356cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,26 @@ # Changelog +## 0.8.7 (2020-07-05) + +* Fix: Fix parsing multipart request body with quoted header parameters (dot net). + (#363 by @ebimmel) + +* Fix: Fix calculating concurrency when `post_max_size` ini is unlimited. + (#365 by @clue) + +* Improve test suite to run tests on PHPUnit 9 and clean up test suite. + (#364 by @SimonFrings) + ## 0.8.6 (2020-01-12) -* Fix parsing Cookie request header with comma in its values (#352 by @fiskie) -* Add .gitattributes to exclude dev files from exports (#353 by @reedy) -* Avoid unneeded warning when decoding invalid data on PHP 7.4 (#357 by @WyriHaximus) +* Fix: Fix parsing `Cookie` request header with comma in its values. + (#352 by @fiskie) + +* Fix: Avoid unneeded warning when decoding invalid data on PHP 7.4. + (#357 by @WyriHaximus) + +* Add .gitattributes to exclude dev files from exports. + (#353 by @reedy) ## 0.8.5 (2019-10-29) diff --git a/README.md b/README.md index 36d80d98..bdeaa50e 100644 --- a/README.md +++ b/README.md @@ -1418,7 +1418,7 @@ The recommended way to install this library is [through Composer](https://getcom This will install the latest supported version: ```bash -$ composer require react/http:^0.8.6 +$ composer require react/http:^0.8.7 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 4ea37ff9e8cbcbcf1b79b34f9c62340eb1a61ec3 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sun, 5 Jul 2020 17:37:34 +0200 Subject: [PATCH 008/152] Add ReactPHP core team as authors to composer.json --- composer.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/composer.json b/composer.json index ab26ee88..22bb4642 100644 --- a/composer.json +++ b/composer.json @@ -3,6 +3,28 @@ "description": "Event-driven, streaming plaintext HTTP and secure HTTPS server for ReactPHP", "keywords": ["event-driven", "streaming", "HTTP", "HTTPS", "server", "ReactPHP"], "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "homepage": "https://clue.engineering/", + "email": "christian@clue.engineering" + }, + { + "name": "Cees-Jan Kiewiet", + "homepage": "https://wyrihaximus.net/", + "email": "reactphp@ceesjankiewiet.nl" + }, + { + "name": "Jan Sorgalla", + "homepage": "https://sorgalla.com/", + "email": "jsorgalla@gmail.com" + }, + { + "name": "Chris Boden", + "homepage": "https://cboden.dev/", + "email": "cboden@gmail.com" + } + ], "require": { "php": ">=5.3.0", "ringcentral/psr7": "^1.2", From ebd66e8531ac14889a2510b375bf0b7d971405f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 6 Jul 2020 18:23:04 +0200 Subject: [PATCH 009/152] Move middleware implementations to new API section --- README.md | 111 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 61 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index bdeaa50e..1d74a43d 100644 --- a/README.md +++ b/README.md @@ -8,28 +8,32 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Quickstart example](#quickstart-example) * [Usage](#usage) - * [Server](#server) - * [StreamingServer](#streamingserver) - * [listen()](#listen) - * [Request](#request) - * [Request parameters](#request-parameters) - * [Query parameters](#query-parameters) - * [Request body](#request-body) - * [Streaming request](#streaming-request) - * [Request method](#request-method) - * [Cookie parameters](#cookie-parameters) - * [Invalid request](#invalid-request) - * [Response](#response) - * [Deferred response](#deferred-response) - * [Streaming response](#streaming-response) - * [Response length](#response-length) - * [Invalid response](#invalid-response) - * [Default response headers](#default-response-headers) - * [Middleware](#middleware) - * [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) - * [RequestBodyBufferMiddleware](#requestbodybuffermiddleware) - * [RequestBodyParserMiddleware](#requestbodyparsermiddleware) - * [Third-Party Middleware](#third-party-middleware) + * [Server](#server) + * [StreamingServer](#streamingserver) + * [listen()](#listen) + * [Request](#request) + * [Request parameters](#request-parameters) + * [Query parameters](#query-parameters) + * [Request body](#request-body) + * [Streaming request](#streaming-request) + * [Request method](#request-method) + * [Cookie parameters](#cookie-parameters) + * [Invalid request](#invalid-request) + * [Response](#response) + * [Deferred response](#deferred-response) + * [Streaming response](#streaming-response) + * [Response length](#response-length) + * [Invalid response](#invalid-response) + * [Default response headers](#default-response-headers) + * [Middleware](#middleware) + * [Custom middleware](#custom-middleware) + * [Third-Party Middleware](#third-party-middleware) +* [API](#api) + * [React\Http\Middleware](#reacthttpmiddleware) + * [StreamingRequestMiddleware](#streamingrequestmiddleware) + * [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) + * [RequestBodyBufferMiddleware](#requestbodybuffermiddleware) + * [RequestBodyParserMiddleware](#requestbodyparsermiddleware) * [Install](#install) * [Tests](#tests) * [License](#license) @@ -1046,6 +1050,8 @@ Many common use cases involve validating, processing, manipulating the incoming HTTP request before passing it to the final business logic request handler. As such, this project supports the concept of middleware request handlers. +#### Custom middleware + A middleware request handler is expected to adhere the following rules: * It is a valid `callable`. @@ -1160,6 +1166,39 @@ $server = new Server(array( )); ``` +#### Third-Party Middleware + +While this project does provide the means to *use* middleware implementations +(see above), it does not aim to *define* how middleware implementations should +look like. We realize that there's a vivid ecosystem of middleware +implementations and ongoing effort to standardize interfaces between these with +[PSR-15](https://www.php-fig.org/psr/psr-15/) (HTTP Server Request Handlers) +and support this goal. +As such, this project only bundles a few middleware implementations that are +required to match PHP's request behavior (see +[middleware implementations](#react-http-middleware)) and otherwise actively +encourages third-party middleware implementations. + +While we would love to support PSR-15 directly in `react/http`, we understand +that this interface does not specifically target async APIs and as such does +not take advantage of promises for [deferred responses](#deferred-response). +The gist of this is that where PSR-15 enforces a `ResponseInterface` return +value, we also accept a `PromiseInterface`. +As such, we suggest using the external +[PSR-15 middleware adapter](https://github.com/friends-of-reactphp/http-middleware-psr15-adapter) +that uses on the fly monkey patching of these return values which makes using +most PSR-15 middleware possible with this package without any changes required. + +Other than that, you can also use the above [middleware definition](#middleware) +to create custom middleware. A non-exhaustive list of third-party middleware can +be found at the [middleware wiki](https://github.com/reactphp/reactphp/wiki/Users#http-middleware). +If you build or know a custom middleware, make sure to let the world know and +feel free to add it to this list. + +## API + +### React\Http\Middleware + #### LimitConcurrentRequestsMiddleware The `LimitConcurrentRequestsMiddleware` can be used to @@ -1382,34 +1421,6 @@ new RequestBodyParserMiddleware(10 * 1024, 100); // 100 files with 10 KiB each If you want to respect this setting, you have to check its value and effectively avoid using this middleware entirely. -#### Third-Party Middleware - -While this project does provide the means to *use* middleware implementations -(see above), it does not aim to *define* how middleware implementations should -look like. We realize that there's a vivid ecosystem of middleware -implementations and ongoing effort to standardize interfaces between these with -[PSR-15](https://www.php-fig.org/psr/psr-15/) (HTTP Server Request Handlers) -and support this goal. -As such, this project only bundles a few middleware implementations that are -required to match PHP's request behavior (see above) and otherwise actively -encourages third-party middleware implementations. - -While we would love to support PSR-15 directy in `react/http`, we understand -that this interface does not specifically target async APIs and as such does -not take advantage of promises for [deferred responses](#deferred-response). -The gist of this is that where PSR-15 enforces a `ResponseInterface` return -value, we also accept a `PromiseInterface`. -As such, we suggest using the external -[PSR-15 middleware adapter](https://github.com/friends-of-reactphp/http-middleware-psr15-adapter) -that uses on the fly monkey patching of these return values which makes using -most PSR-15 middleware possible with this package without any changes required. - -Other than that, you can also use the above [middleware definition](#middleware) -to create custom middleware. A non-exhaustive list of third-party middleware can -be found at the [middleware wiki](https://github.com/reactphp/http/wiki/Middleware). -If you build or know a custom middleware, make sure to let the world know and -feel free to add it to this list. - ## Install The recommended way to install this library is [through Composer](https://getcomposer.org). From 05e5fadf59b3757bed36927063e8b8ae6af9099c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 6 Jul 2020 12:33:14 +0200 Subject: [PATCH 010/152] Add `StreamingRequestMiddleware` to stream incoming requests This middleware can be used to process incoming requests with a streaming request body (without buffering). This will replace the existing `StreamingServer` class. --- README.md | 82 ++++++++++++++++--- src/Middleware/StreamingRequestMiddleware.php | 69 ++++++++++++++++ src/Server.php | 35 +++++--- .../StreamingRequestMiddlewareTest.php | 23 ++++++ tests/ServerTest.php | 33 ++++++++ 5 files changed, 220 insertions(+), 22 deletions(-) create mode 100644 src/Middleware/StreamingRequestMiddleware.php create mode 100644 tests/Middleware/StreamingRequestMiddlewareTest.php diff --git a/README.md b/README.md index 1d74a43d..98d2968f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](https://reactphp.org/). -**Table of Contents** +**Table of contents** * [Quickstart example](#quickstart-example) * [Usage](#usage) @@ -15,13 +15,13 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Request parameters](#request-parameters) * [Query parameters](#query-parameters) * [Request body](#request-body) - * [Streaming request](#streaming-request) + * [Streaming incoming request](#streaming-incoming-request) * [Request method](#request-method) * [Cookie parameters](#cookie-parameters) * [Invalid request](#invalid-request) * [Response](#response) * [Deferred response](#deferred-response) - * [Streaming response](#streaming-response) + * [Streaming outgoing response](#streaming-outgoing-response) * [Response length](#response-length) * [Invalid response](#invalid-response) * [Default response headers](#default-response-headers) @@ -220,7 +220,7 @@ in memory. It will invoke the request handler function once the HTTP request headers have been received, i.e. before receiving the potentially much larger HTTP request body. This means the [request](#request) passed to your request handler function may not be fully compatible with PSR-7. See also -[streaming request](#streaming-request) below for more details. +[streaming incoming request](#streaming-incoming-request) below for more details. ### listen() @@ -389,7 +389,7 @@ This includes the parsed request body and any file uploads. > If you're using the advanced [`StreamingServer`](#streamingserver) class, jump to the next chapter to learn more about how to process a - [streaming request](#streaming-request). + [streaming incoming request](#streaming-incoming-request). As stated above, each incoming HTTP request is always represented by the [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface). @@ -485,7 +485,9 @@ header or when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. intermediary `HTTP/1.1 100 Continue` response to the client. This ensures you will receive the request body without a delay as expected. -#### Streaming request +#### Streaming incoming request + + If you're using the advanced [`StreamingServer`](#streamingserver), the request object will be processed once the request headers have been received. @@ -784,7 +786,9 @@ The promise cancellation handler can be used to clean up any pending resources allocated in this case (if applicable). If a promise is resolved after the client closes, it will simply be ignored. -#### Streaming response +#### Streaming outgoing response + + The `Response` class in this project supports to add an instance which implements the [ReactPHP ReadableStreamInterface](https://github.com/reactphp/stream#readablestreaminterface) @@ -897,7 +901,7 @@ $server = new Server(function (ServerRequestInterface $request) { ``` If the response body size is unknown, a `Content-Length` response header can not -be added automatically. When using a [streaming response](#streaming-response) +be added automatically. When using a [streaming outgoing response](#streaming-outgoing-response) without an explicit `Content-Length` response header, outgoing HTTP/1.1 response messages will automatically use `Transfer-Encoding: chunked` while legacy HTTP/1.0 response messages will contain the plain response body. If you know the length @@ -959,8 +963,8 @@ $server->on('error', function (Exception $e) { Note that the server will also emit an `error` event if the client sends an invalid HTTP request that never reaches your request handler function. See also [invalid request](#invalid-request) for more details. -Additionally, a [streaming request](#streaming-request) body can also emit -an `error` event on the request body. +Additionally, a [streaming incoming request](#streaming-incoming-request) body +can also emit an `error` event on the request body. The server will only send a very generic `500` (Interval Server Error) HTTP error response without any further details to the client if an unhandled @@ -1199,6 +1203,64 @@ feel free to add it to this list. ### React\Http\Middleware +#### StreamingRequestMiddleware + +The `StreamingRequestMiddleware` can be used to +process incoming requests with a streaming request body (without buffering). + +This allows you to process requests of any size without buffering the request +body in memory. Instead, it will represent the request body as a +[`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +that emit chunks of incoming data as it is received: + +```php +$server = new React\Http\Server(array( + new React\Http\Middleware\StreamingRequestMiddleware(), + function (Psr\Http\Message\ServerRequestInterface $request) { + $body = $request->getBody(); + assert($body instanceof Psr\Http\Message\StreamInterface); + assert($body instanceof React\Stream\ReadableStreamInterface); + + return new React\Promise\Promise(function ($resolve) use ($body) { + $bytes = 0; + $body->on('data', function ($chunk) use (&$bytes) { + $bytes += \count($chunk); + }); + $body->on('close', function () use (&$bytes, $resolve) { + $resolve(new React\Http\Response( + 200, + [], + "Received $bytes bytes\n" + )); + }); + }); + } +)); +``` + +See also [streaming incoming request](#streaming-incoming-request) +for more details. + +Additionally, this middleware can be used in combination with the +[`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and +[`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) +to explicitly configure the total number of requests that can be handled at +once: + +```php +$server = new React\Http\Server(array( + new React\Http\Middleware\StreamingRequestMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + new React\Http\Middleware\RequestBodyParserMiddleware(), + $handler +)); +``` + +> Internally, this class is used as a "marker" to not trigger the default + request buffering behavior in the `Server`. It does not implement any logic + on its own. + #### LimitConcurrentRequestsMiddleware The `LimitConcurrentRequestsMiddleware` can be used to diff --git a/src/Middleware/StreamingRequestMiddleware.php b/src/Middleware/StreamingRequestMiddleware.php new file mode 100644 index 00000000..a68454f6 --- /dev/null +++ b/src/Middleware/StreamingRequestMiddleware.php @@ -0,0 +1,69 @@ +getBody(); + * assert($body instanceof Psr\Http\Message\StreamInterface); + * assert($body instanceof React\Stream\ReadableStreamInterface); + * + * return new React\Promise\Promise(function ($resolve) use ($body) { + * $bytes = 0; + * $body->on('data', function ($chunk) use (&$bytes) { + * $bytes += \count($chunk); + * }); + * $body->on('close', function () use (&$bytes, $resolve) { + * $resolve(new React\Http\Response( + * 200, + * [], + * "Received $bytes bytes\n" + * )); + * }); + * }); + * } + * )); + * ``` + * + * See also [streaming incoming request](../../README.md#streaming-incoming-request) + * for more details. + * + * Additionally, this middleware can be used in combination with the + * [`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and + * [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) + * to explicitly configure the total number of requests that can be handled at + * once: + * + * ```php + * $server = new React\Http\Server(array( + * new React\Http\Middleware\StreamingRequestMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + * new React\Http\Middleware\RequestBodyParserMiddleware(), + * $handler + * )); + * ``` + * + * > Internally, this class is used as a "marker" to not trigger the default + * request buffering behavior in the `Server`. It does not implement any logic + * on its own. + */ +final class StreamingRequestMiddleware +{ + public function __invoke(ServerRequestInterface $request, $next) + { + return $next($request); + } +} diff --git a/src/Server.php b/src/Server.php index cf3f9cac..8d159a0a 100644 --- a/src/Server.php +++ b/src/Server.php @@ -5,6 +5,7 @@ use Evenement\EventEmitter; use React\Http\Io\IniUtil; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; +use React\Http\Middleware\StreamingRequestMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\RequestBodyParserMiddleware; use React\Socket\ServerInterface; @@ -126,19 +127,29 @@ public function __construct($requestHandler) throw new \InvalidArgumentException('Invalid request handler given'); } + $streaming = false; + foreach ((array) $requestHandler as $handler) { + if ($handler instanceof StreamingRequestMiddleware) { + $streaming = true; + break; + } + } + $middleware = array(); - $middleware[] = new LimitConcurrentRequestsMiddleware( - $this->getConcurrentRequestsLimit(\ini_get('memory_limit'), \ini_get('post_max_size')) - ); - $middleware[] = new RequestBodyBufferMiddleware(); - // Checking for an empty string because that is what a boolean - // false is returned as by ini_get depending on the PHP version. - // @link http://php.net/manual/en/ini.core.php#ini.enable-post-data-reading - // @link http://php.net/manual/en/function.ini-get.php#refsect1-function.ini-get-notes - // @link https://3v4l.org/qJtsa - $enablePostDataReading = \ini_get('enable_post_data_reading'); - if ($enablePostDataReading !== '') { - $middleware[] = new RequestBodyParserMiddleware(); + if (!$streaming) { + $middleware[] = new LimitConcurrentRequestsMiddleware( + $this->getConcurrentRequestsLimit(\ini_get('memory_limit'), \ini_get('post_max_size')) + ); + $middleware[] = new RequestBodyBufferMiddleware(); + // Checking for an empty string because that is what a boolean + // false is returned as by ini_get depending on the PHP version. + // @link http://php.net/manual/en/ini.core.php#ini.enable-post-data-reading + // @link http://php.net/manual/en/function.ini-get.php#refsect1-function.ini-get-notes + // @link https://3v4l.org/qJtsa + $enablePostDataReading = \ini_get('enable_post_data_reading'); + if ($enablePostDataReading !== '') { + $middleware[] = new RequestBodyParserMiddleware(); + } } if (\is_callable($requestHandler)) { diff --git a/tests/Middleware/StreamingRequestMiddlewareTest.php b/tests/Middleware/StreamingRequestMiddlewareTest.php new file mode 100644 index 00000000..c78ab3af --- /dev/null +++ b/tests/Middleware/StreamingRequestMiddlewareTest.php @@ -0,0 +1,23 @@ +assertSame($response, $ret); + } +} diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 4ca81e16..02845769 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -8,6 +8,8 @@ use React\Promise\Deferred; use Clue\React\Block; use React\Promise; +use React\Http\Middleware\StreamingRequestMiddleware; +use React\Stream\ReadableStreamInterface; final class ServerTest extends TestCase { @@ -142,6 +144,37 @@ public function testPostFileUpload() $this->assertSame("hello\r\n", (string)$files['file']->getStream()); } + public function testServerReceivesBufferedRequestByDefault() + { + $streaming = null; + $server = new Server(function (ServerRequestInterface $request) use (&$streaming) { + $streaming = $request->getBody() instanceof ReadableStreamInterface; + }); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); + + $this->assertEquals(false, $streaming); + } + + public function testServerWithStreamingRequestMiddlewareReceivesStreamingRequest() + { + $streaming = null; + $server = new Server(array( + new StreamingRequestMiddleware(), + function (ServerRequestInterface $request) use (&$streaming) { + $streaming = $request->getBody() instanceof ReadableStreamInterface; + } + )); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); + + $this->assertEquals(true, $streaming); + } + public function testForwardErrors() { $exception = new \Exception(); From f3efa6bedba88b53234a5ce24c128c248ce5597b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 6 Jul 2020 22:12:00 +0200 Subject: [PATCH 011/152] Mark `StreamingServer` as internal, use `StreamingRequestMiddleware` --- README.md | 365 +++++++++--------- examples/08-stream-response.php | 2 - examples/12-upload.php | 8 +- examples/13-stream-request.php | 72 ++-- examples/21-http-proxy.php | 4 +- examples/22-connect-proxy.php | 2 +- examples/31-upgrade-echo.php | 2 +- examples/32-upgrade-chat.php | 2 +- examples/99-benchmark-download.php | 2 - src/{ => Io}/StreamingServer.php | 53 +-- .../LimitConcurrentRequestsMiddleware.php | 9 +- src/Server.php | 169 ++++++-- tests/FunctionalServerTest.php | 67 ++-- tests/{ => Io}/StreamingServerTest.php | 6 +- 14 files changed, 406 insertions(+), 357 deletions(-) rename src/{ => Io}/StreamingServer.php (89%) rename tests/{ => Io}/StreamingServerTest.php (99%) diff --git a/README.md b/README.md index 98d2968f..d53e1540 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Quickstart example](#quickstart-example) * [Usage](#usage) * [Server](#server) - * [StreamingServer](#streamingserver) * [listen()](#listen) * [Request](#request) * [Request parameters](#request-parameters) @@ -70,15 +69,14 @@ See also the [examples](examples). The `Server` class is responsible for handling incoming connections and then processing each incoming HTTP request. -It buffers and parses the complete incoming HTTP request in memory. Once the -complete request has been received, it will invoke the request handler function. -This request handler function needs to be passed to the constructor and will be -invoked with the respective [request](#request) object and expects a -[response](#response) object in return: +When a complete HTTP request has been received, it will invoke the given +request handler function. This request handler function needs to be passed to +the constructor and will be invoked with the respective [request](#request) +object and expects a [response](#response) object in return: ```php -$server = new Server(function (ServerRequestInterface $request) { - return new Response( +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { + return new React\Http\Response( 200, array( 'Content-Type' => 'text/plain' @@ -91,35 +89,36 @@ $server = new Server(function (ServerRequestInterface $request) { Each incoming HTTP request message is always represented by the [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), see also following [request](#request) chapter for more details. + Each outgoing HTTP response message is always represented by the [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), see also following [response](#response) chapter for more details. -In order to process any connections, the server needs to be attached to an -instance of `React\Socket\ServerInterface` through the [`listen()`](#listen) method -as described in the following chapter. In its most simple form, you can attach -this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) -in order to start a plaintext HTTP server like this: +In order to start listening for any incoming connections, the `Server` needs +to be attached to an instance of +[`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) +through the [`listen()`](#listen) method as described in the following +chapter. In its most simple form, you can attach this to a +[`React\Socket\Server`](https://github.com/reactphp/socket#server) in order +to start a plaintext HTTP server like this: ```php -$server = new Server($handler); +$server = new React\Http\Server($handler); $socket = new React\Socket\Server('0.0.0.0:8080', $loop); $server->listen($socket); ``` -See also the [`listen()`](#listen) method and the [first example](examples) for more details. - -The `Server` class is built as a facade around the underlying -[`StreamingServer`](#streamingserver) to provide sane defaults for 80% of the -use cases and is the recommended way to use this library unless you're sure -you know what you're doing. +See also the [`listen()`](#listen) method and the [first example](../examples/) +for more details. -Unlike the underlying [`StreamingServer`](#streamingserver), this class -buffers and parses the complete incoming HTTP request in memory. Once the -complete request has been received, it will invoke the request handler -function. This means the [request](#request) passed to your request handler -function will be fully compatible with PSR-7. +By default, the `Server` buffers and parses the complete incoming HTTP +request in memory. It will invoke the given request handler function when the +complete request headers and request body has been received. This means the +[request](#request) object passed to your request handler function will be +fully compatible with PSR-7 (http-message). This provides sane defaults for +80% of the use cases and is the recommended way to use this library unless +you're sure you know what you're doing. On the other hand, buffering complete HTTP requests in memory until they can be processed by your request handler function means that this class has to @@ -141,9 +140,9 @@ upload_max_filesize 2M max_file_uploads 20 ``` -In particular, the `post_max_size` setting limits how much memory a single HTTP -request is allowed to consume while buffering its request body. On top of -this, this class will try to avoid consuming more than 1/4 of your +In particular, the `post_max_size` setting limits how much memory a single +HTTP request is allowed to consume while buffering its request body. On top +of this, this class will try to avoid consuming more than 1/4 of your `memory_limit` for buffering multiple concurrent HTTP requests. As such, with the above default settings of `128M` max, it will try to consume no more than `32M` for buffering multiple concurrent HTTP requests. As a consequence, it @@ -151,116 +150,93 @@ will limit the concurrency to 4 HTTP requests with the above defaults. It is imperative that you assign reasonable values to your PHP ini settings. It is usually recommended to either reduce the memory a single request is -allowed to take (set `post_max_size 1M` or less) or to increase the total memory -limit to allow for more concurrent requests (set `memory_limit 512M` or more). -Failure to do so means that this class may have to disable concurrency and -only handle one request at a time. - -Internally, this class automatically assigns these limits to the -[middleware](#middleware) request handlers as described below. For more -advanced use cases, you may also use the advanced -[`StreamingServer`](#streamingserver) and assign these middleware request -handlers yourself as described in the following chapters. +allowed to take (set `post_max_size 1M` or less) or to increase the total +memory limit to allow for more concurrent requests (set `memory_limit 512M` +or more). Failure to do so means that this class may have to disable +concurrency and only handle one request at a time. -### StreamingServer - -The advanced `StreamingServer` class is responsible for handling incoming connections and then -processing each incoming HTTP request. - -Unlike the [`Server`](#server) class, it does not buffer and parse the incoming -HTTP request body by default. This means that the request handler will be -invoked with a streaming request body. Once the request headers have been -received, it will invoke the request handler function. This request handler -function needs to be passed to the constructor and will be invoked with the -respective [request](#request) object and expects a [response](#response) -object in return: +As an alternative to the above buffering defaults, you can also configure +the `Server` explicitly to override these defaults. You can use the +[`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and +[`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) +to explicitly configure the total number of requests that can be handled at +once like this: ```php -$server = new StreamingServer(function (ServerRequestInterface $request) { - return new Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - "Hello World!\n" - ); -}); +$server = new React\Http\Server(array( + new React\Http\Middleware\StreamingRequestMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + new React\Http\Middleware\RequestBodyParserMiddleware(), + $handler +)); ``` -Each incoming HTTP request message is always represented by the -[PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), -see also following [request](#request) chapter for more details. -Each outgoing HTTP response message is always represented by the -[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), -see also following [response](#response) chapter for more details. +> Internally, this class automatically assigns these middleware handlers + automatically when no [`StreamingRequestMiddleware`](#streamingrequestmiddleware) + is given. Accordingly, you can use this example to override all default + settings to implement custom limits. -In order to process any connections, the server needs to be attached to an -instance of `React\Socket\ServerInterface` through the [`listen()`](#listen) method -as described in the following chapter. In its most simple form, you can attach -this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) -in order to start a plaintext HTTP server like this: +As an alternative to buffering the complete request body in memory, you can +also use a streaming approach where only small chunks of data have to be kept +in memory: ```php -$server = new StreamingServer($handler); - -$socket = new React\Socket\Server('0.0.0.0:8080', $loop); -$server->listen($socket); +$server = new React\Http\Server(array( + new React\Http\Middleware\StreamingRequestMiddleware(), + $handler +)); ``` -See also the [`listen()`](#listen) method and the [first example](examples) for more details. - -The `StreamingServer` class is considered advanced usage and unless you know -what you're doing, you're recommended to use the [`Server`](#server) class -instead. The `StreamingServer` class is specifically designed to help with -more advanced use cases where you want to have full control over consuming -the incoming HTTP request body and concurrency settings. - -In particular, this class does not buffer and parse the incoming HTTP request -in memory. It will invoke the request handler function once the HTTP request -headers have been received, i.e. before receiving the potentially much larger -HTTP request body. This means the [request](#request) passed to your request -handler function may not be fully compatible with PSR-7. See also -[streaming incoming request](#streaming-incoming-request) below for more details. +In this case, it will invoke the request handler function once the HTTP +request headers have been received, i.e. before receiving the potentially +much larger HTTP request body. This means the [request](#request) passed to +your request handler function may not be fully compatible with PSR-7. This is +specifically designed to help with more advanced use cases where you want to +have full control over consuming the incoming HTTP request body and +concurrency settings. See also [streaming incoming request](#streaming-incoming-request) +below for more details. ### listen() The `listen(React\Socket\ServerInterface $socket): void` method can be used to -start processing connections from the given socket server. +start listening for HTTP requests on the given socket server instance. + The given [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) -is responsible for emitting the underlying streaming connections. -This HTTP server needs to be attached to it in order to process any connections -and pase incoming streaming data as incoming HTTP request messages. -In its most common form, you can attach this to a -[`React\Socket\Server`](https://github.com/reactphp/socket#server) -in order to start a plaintext HTTP server like this: +is responsible for emitting the underlying streaming connections. This +HTTP server needs to be attached to it in order to process any +connections and pase incoming streaming data as incoming HTTP request +messages. In its most common form, you can attach this to a +[`React\Socket\Server`](https://github.com/reactphp/socket#server) in +order to start a plaintext HTTP server like this: ```php -$server = new Server($handler); -// or -$server = new StreamingServer($handler); +$server = new React\Http\Server($handler); $socket = new React\Socket\Server('0.0.0.0:8080', $loop); $server->listen($socket); ``` -This example will start listening for HTTP requests on the alternative HTTP port -`8080` on all interfaces (publicly). As an alternative, it is very common to use -a reverse proxy and let this HTTP server listen on the localhost (loopback) -interface only by using the listen address `127.0.0.1:8080` instead. This way, you -host your application(s) on the default HTTP port `80` and only route specific -requests to this HTTP server. +See also [example #1](examples) for more details. + +This example will start listening for HTTP requests on the alternative +HTTP port `8080` on all interfaces (publicly). As an alternative, it is +very common to use a reverse proxy and let this HTTP server listen on the +localhost (loopback) interface only by using the listen address +`127.0.0.1:8080` instead. This way, you host your application(s) on the +default HTTP port `80` and only route specific requests to this HTTP +server. Likewise, it's usually recommended to use a reverse proxy setup to accept -secure HTTPS requests on default HTTPS port `443` (TLS termination) and only -route plaintext requests to this HTTP server. As an alternative, you can also -accept secure HTTPS requests with this HTTP server by attaching this to a -[`React\Socket\Server`](https://github.com/reactphp/socket#server) using a -secure TLS listen address, a certificate file and optional `passphrase` like this: +secure HTTPS requests on default HTTPS port `443` (TLS termination) and +only route plaintext requests to this HTTP server. As an alternative, you +can also accept secure HTTPS requests with this HTTP server by attaching +this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) +using a secure TLS listen address, a certificate file and optional +`passphrase` like this: ```php -$server = new Server($handler); -// or -$server = new StreamingServer($handler); +$server = new React\Http\Server($handler); $socket = new React\Socket\Server('tls://0.0.0.0:8443', $loop, array( 'local_cert' => __DIR__ . '/localhost.pem' @@ -272,9 +248,8 @@ See also [example #11](examples) for more details. ### Request -As seen above, the [`Server`](#server) and [`StreamingServer`](#streamingserver) -classes are responsible for handling incoming connections and then processing -each incoming HTTP request. +As seen above, the [`Server`](#server) class is responsible for handling +incoming connections and then processing each incoming HTTP request. The request object will be processed once the request has been received by the client. @@ -383,12 +358,13 @@ See also [example #4](examples). #### Request body -If you're using the [`Server`](#server), then the request object will be -buffered and parsed in memory and contains the full request body. -This includes the parsed request body and any file uploads. +By default, the [`Server`](#server) will buffer and parse the full request body +in memory. This means the given request object includes the parsed request body +and any file uploads. -> If you're using the advanced [`StreamingServer`](#streamingserver) class, jump - to the next chapter to learn more about how to process a +> As an alternative to the default buffering logic, you can also use the + [`StreamingRequestMiddleware`](#streamingrequestmiddleware). Jump to the next + chapter to learn more about how to process a [streaming incoming request](#streaming-incoming-request). As stated above, each incoming HTTP request is always represented by the @@ -489,13 +465,13 @@ header or when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. -If you're using the advanced [`StreamingServer`](#streamingserver), the -request object will be processed once the request headers have been received. +If you're using the advanced [`StreamingRequestMiddleware`](#streamingrequestmiddleware), +the request object will be processed once the request headers have been received. This means that this happens irrespective of (i.e. *before*) receiving the (potentially much larger) request body. -> If you're using the [`Server`](#server) class, jump to the previous chapter - to learn more about how to process a buffered [request body](#request-body). +> Note that this is non-standard behavior considered advanced usage. Jump to the + previous chapter to learn more about how to process a buffered [request body](#request-body). While this may be uncommon in the PHP ecosystem, this is actually a very powerful approach that gives you several advantages not otherwise possible: @@ -525,37 +501,42 @@ The ReactPHP `ReadableStreamInterface` gives you access to the incoming request body as the individual chunks arrive: ```php -$server = new StreamingServer(function (ServerRequestInterface $request) { - return new Promise(function ($resolve, $reject) use ($request) { - $contentLength = 0; - $request->getBody()->on('data', function ($data) use (&$contentLength) { - $contentLength += strlen($data); - }); +$server = new React\Http\Server(array( + new React\Http\Middleware\StreamingRequestMiddleware(), + function (Psr\Http\Message\ServerRequestInterface $request) { + $body = $request->getBody(); + assert($body instanceof Psr\Http\Message\StreamInterface); + assert($body instanceof React\Stream\ReadableStreamInterface); - $request->getBody()->on('end', function () use ($resolve, &$contentLength){ - $response = new Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - "The length of the submitted request body is: " . $contentLength - ); - $resolve($response); - }); + return new React\Promise\Promise(function ($resolve, $reject) use ($body) { + $bytes = 0; + $body->on('data', function ($data) use (&$bytes) { + $bytes += strlen($data); + }); - // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event - $request->getBody()->on('error', function (\Exception $exception) use ($resolve, &$contentLength) { - $response = new Response( - 400, - array( - 'Content-Type' => 'text/plain' - ), - "An error occured while reading at length: " . $contentLength - ); - $resolve($response); + $body->on('end', function () use ($resolve, &$bytes){ + $resolve(new React\Http\Response( + 200, + array( + 'Content-Type' => 'text/plain' + ), + "Received $bytes bytes\n" + )); + }); + + // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event + $body->on('error', function (\Exception $exception) use ($resolve, &$bytes) { + $resolve(new React\Http\Response( + 400, + array( + 'Content-Type' => 'text/plain' + ), + "Encountered error after $bytes bytes: {$exception->getMessage()}\n" + )); + }); }); - }); -}); + } +)); ``` The above example simply counts the number of bytes received in the request body. @@ -593,32 +574,35 @@ This method operates on the streaming request body, i.e. the request body size may be unknown (`null`) when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. ```php -$server = new StreamingServer(function (ServerRequestInterface $request) { - $size = $request->getBody()->getSize(); - if ($size === null) { - $body = 'The request does not contain an explicit length.'; - $body .= 'This example does not accept chunked transfer encoding.'; +$server = new React\Http\Server(array( + new React\Http\Middleware\StreamingRequestMiddleware(), + function (Psr\Http\Message\ServerRequestInterface $request) { + $size = $request->getBody()->getSize(); + if ($size === null) { + $body = 'The request does not contain an explicit length.'; + $body .= 'This example does not accept chunked transfer encoding.'; - return new Response( - 411, + return new React\Http\Response( + 411, + array( + 'Content-Type' => 'text/plain' + ), + $body + ); + } + + return new React\Http\Response( + 200, array( 'Content-Type' => 'text/plain' ), - $body + "Request body size: " . $size . " bytes\n" ); } - - return new Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - "Request body size: " . $size . " bytes\n" - ); -}); +)); ``` -> Note: The `StreamingServer` automatically takes care of handling requests with the +> Note: The `Server` automatically takes care of handling requests with the additional `Expect: 100-continue` request header. When HTTP/1.1 clients want to send a bigger request body, they MAY send only the request headers with an additional `Expect: 100-continue` request header and wait before sending the actual @@ -700,8 +684,8 @@ See also [example #5](examples) for more details. #### Invalid request -The `Server` and `StreamingServer` classes support both HTTP/1.1 and HTTP/1.0 request -messages. If a client sends an invalid request message, uses an invalid HTTP +The `Server` class supports both HTTP/1.1 and HTTP/1.0 request messages. +If a client sends an invalid request message, uses an invalid HTTP protocol version or sends an invalid `Transfer-Encoding` request header value, the server will automatically send a `400` (Bad Request) HTTP error response to the client and close the connection. @@ -720,10 +704,9 @@ valid response object from your request handler function. See also ### Response -The callback function passed to the constructor of the [`Server`](#server) or -advanced [`StreamingServer`](#server) is responsible for processing the request -and returning a response, which will be delivered to the client. -This function MUST return an instance implementing +The callback function passed to the constructor of the [`Server`](#server) is +responsible for processing the request and returning a response, which will be +delivered to the client. This function MUST return an instance implementing [PSR-7 ResponseInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#33-psrhttpmessageresponseinterface) object or a [ReactPHP Promise](https://github.com/reactphp/promise#reactpromise) @@ -977,9 +960,11 @@ create your own HTTP response message instead. #### Default response headers -After the return in the callback function the response will be processed by the -[`Server`](#server) or [`StreamingServer`](#streamingserver) respectively. -They will add the protocol version of the request, so you don't have to. +When a response is returned from the request handler function, it will be +processed by the [`Server`](#server) and then sent back to the client. + +The `Server` will automatically add the protocol version of the request, so you +don't have to. A `Date` header will be automatically added with the system date and time if none is given. You can add a custom `Date` header yourself like this: @@ -1045,10 +1030,9 @@ passed explicitly. ### Middleware -As documented above, the [`Server`](#server) and advanced -[`StreamingServer`](#streamingserver) accept a single -request handler argument that is responsible for processing an incoming -HTTP request and then creating and returning an outgoing HTTP response. +As documented above, the [`Server`](#server) accepts a single request handler +argument that is responsible for processing an incoming HTTP request and then +creating and returning an outgoing HTTP response. Many common use cases involve validating, processing, manipulating the incoming HTTP request before passing it to the final business logic request handler. @@ -1096,8 +1080,7 @@ required to match PHP's request behavior (see below) and otherwise actively encourages [Third-Party Middleware](#third-party-middleware) implementations. In order to use middleware request handlers, simply pass an array with all -callables as defined above to the [`Server`](#server) or -[`StreamingServer`](#streamingserver) respectively. +callables as defined above to the [`Server`](#server). The following example adds a middleware request handler that adds the current time to the request as a header (`Request-Time`) and a final request handler that always returns a 200 code without a body: @@ -1292,7 +1275,8 @@ Similarly, this middleware is often used in combination with the to limit the total number of requests that can be buffered at once: ```php -$server = new StreamingServer(array( +$server = new Server(array( + new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request new RequestBodyParserMiddleware(), @@ -1305,7 +1289,8 @@ that can be buffered at once and then ensure the actual request handler only processes one request after another without any concurrency: ```php -$server = new StreamingServer(array( +$server = new Server(array( + new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request new RequestBodyParserMiddleware(), @@ -1357,7 +1342,8 @@ the total number of concurrent requests. Usage: ```php -$server = new StreamingServer(array( +$server = new Server(array( + new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (ServerRequestInterface $request) { @@ -1416,7 +1402,8 @@ $handler = function (ServerRequestInterface $request) { ); }; -$server = new StreamingServer(array(( +$server = new Server(array( + new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB new RequestBodyParserMiddleware(), diff --git a/examples/08-stream-response.php b/examples/08-stream-response.php index ab0ea0ec..dce54b2b 100644 --- a/examples/08-stream-response.php +++ b/examples/08-stream-response.php @@ -10,8 +10,6 @@ $loop = Factory::create(); -// Note how this example still uses `Server` instead of `StreamingServer`. -// The `StreamingServer` is only required for streaming *incoming* requests. $server = new Server(function (ServerRequestInterface $request) use ($loop) { if ($request->getMethod() !== 'GET' || $request->getUri()->getPath() !== '/') { return new Response(404); diff --git a/examples/12-upload.php b/examples/12-upload.php index 4e21bdb0..38d8c708 100644 --- a/examples/12-upload.php +++ b/examples/12-upload.php @@ -13,8 +13,9 @@ use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\RequestBodyParserMiddleware; +use React\Http\Middleware\StreamingRequestMiddleware; use React\Http\Response; -use React\Http\StreamingServer; +use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; @@ -121,9 +122,10 @@ ); }; -// Note how this example explicitly uses the advanced `StreamingServer` to apply +// Note how this example explicitly uses the advanced `StreamingRequestMiddleware` to apply // custom request buffering limits below before running our request handler. -$server = new StreamingServer(array( +$server = new Server(array( + new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers, queue otherwise new RequestBodyBufferMiddleware(8 * 1024 * 1024), // 8 MiB max, ignore body otherwise new RequestBodyParserMiddleware(100 * 1024, 1), // 1 file with 100 KiB max, reject upload otherwise diff --git a/examples/13-stream-request.php b/examples/13-stream-request.php index 07cd21ed..6b9c8cec 100644 --- a/examples/13-stream-request.php +++ b/examples/13-stream-request.php @@ -1,50 +1,52 @@ getBody(); - $requestBody->on('data', function ($data) use (&$contentLength) { - $contentLength += strlen($data); +$server = new React\Http\Server(array( + new React\Http\Middleware\StreamingRequestMiddleware(), + function (Psr\Http\Message\ServerRequestInterface $request) { + $body = $request->getBody(); + assert($body instanceof Psr\Http\Message\StreamInterface); + assert($body instanceof React\Stream\ReadableStreamInterface); + + return new React\Promise\Promise(function ($resolve, $reject) use ($body) { + $bytes = 0; + $body->on('data', function ($data) use (&$bytes) { + $bytes += strlen($data); + }); + + $body->on('end', function () use ($resolve, &$bytes){ + $resolve(new React\Http\Response( + 200, + array( + 'Content-Type' => 'text/plain' + ), + "Received $bytes bytes\n" + )); + }); + + // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event + $body->on('error', function (\Exception $exception) use ($resolve, &$bytes) { + $resolve(new React\Http\Response( + 400, + array( + 'Content-Type' => 'text/plain' + ), + "Encountered error after $bytes bytes: {$exception->getMessage()}\n" + )); + }); }); + } +)); - $requestBody->on('end', function () use ($resolve, &$contentLength){ - $response = new Response( - 200, - array( - 'Content-Type' => 'text/plain' - ), - "The length of the submitted request body is: " . $contentLength - ); - $resolve($response); - }); - - // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event - $requestBody->on('error', function (\Exception $exception) use ($resolve, &$contentLength) { - $response = new Response( - 400, - array( - 'Content-Type' => 'text/plain' - ), - "An error occured while reading at length: " . $contentLength - ); - $resolve($response); - }); - }); -}); +$server->on('error', 'printf'); $socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); $server->listen($socket); diff --git a/examples/21-http-proxy.php b/examples/21-http-proxy.php index a881c050..1fe32e36 100644 --- a/examples/21-http-proxy.php +++ b/examples/21-http-proxy.php @@ -10,10 +10,10 @@ $loop = Factory::create(); -// Note how this example uses the `Server` instead of `StreamingServer`. +// Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // This means that this proxy buffers the whole request before "processing" it. // As such, this is store-and-forward proxy. This could also use the advanced -// `StreamingServer` to forward the incoming request as it comes in. +// `StreamingRequestMiddleware` to forward the incoming request as it comes in. $server = new Server(function (RequestInterface $request) { if (strpos($request->getRequestTarget(), '://') === false) { return new Response( diff --git a/examples/22-connect-proxy.php b/examples/22-connect-proxy.php index 63c20833..85b1a7c9 100644 --- a/examples/22-connect-proxy.php +++ b/examples/22-connect-proxy.php @@ -12,7 +12,7 @@ $loop = Factory::create(); $connector = new Connector($loop); -// Note how this example uses the `Server` instead of `StreamingServer`. +// Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // Unlike the plain HTTP proxy, the CONNECT method does not contain a body // and we establish an end-to-end connection over the stream object, so this // doesn't have to store any payload data in memory at all. diff --git a/examples/31-upgrade-echo.php b/examples/31-upgrade-echo.php index 21d6eb67..df572d50 100644 --- a/examples/31-upgrade-echo.php +++ b/examples/31-upgrade-echo.php @@ -27,7 +27,7 @@ $loop = Factory::create(); -// Note how this example uses the `Server` instead of `StreamingServer`. +// Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // The initial incoming request does not contain a body and we upgrade to a // stream object below. $server = new Server(function (ServerRequestInterface $request) use ($loop) { diff --git a/examples/32-upgrade-chat.php b/examples/32-upgrade-chat.php index 89230f31..5d60154c 100644 --- a/examples/32-upgrade-chat.php +++ b/examples/32-upgrade-chat.php @@ -35,7 +35,7 @@ // this means that any Upgraded data will simply be sent back to the client $chat = new ThroughStream(); -// Note how this example uses the `Server` instead of `StreamingServer`. +// Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // The initial incoming request does not contain a body and we upgrade to a // stream object below. $server = new Server(function (ServerRequestInterface $request) use ($loop, $chat) { diff --git a/examples/99-benchmark-download.php b/examples/99-benchmark-download.php index 692bd810..393f87b1 100644 --- a/examples/99-benchmark-download.php +++ b/examples/99-benchmark-download.php @@ -86,8 +86,6 @@ public function getSize() } } -// Note how this example still uses `Server` instead of `StreamingServer`. -// The `StreamingServer` is only required for streaming *incoming* requests. $server = new Server(function (ServerRequestInterface $request) use ($loop) { switch ($request->getUri()->getPath()) { case '/': diff --git a/src/StreamingServer.php b/src/Io/StreamingServer.php similarity index 89% rename from src/StreamingServer.php rename to src/Io/StreamingServer.php index 3826dbc3..30ab7705 100644 --- a/src/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -1,18 +1,11 @@ listen($socket); - * ``` - * - * See also [example #1](examples) for more details. - * - * Similarly, you can also attach this to a - * [`React\Socket\SecureServer`](https://github.com/reactphp/socket#secureserver) - * in order to start a secure HTTPS server like this: - * - * ```php - * $server = new StreamingServer($handler); - * - * $socket = new React\Socket\Server(8080, $loop); - * $socket = new React\Socket\SecureServer($socket, $loop, array( - * 'local_cert' => __DIR__ . '/localhost.pem' - * )); - * - * $server->listen($socket); - * ``` - * - * See also [example #11](examples) for more details. - * * @param ServerInterface $socket + * @see \React\Http\Server::listen() */ public function listen(ServerInterface $socket) { diff --git a/src/Middleware/LimitConcurrentRequestsMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php index f816f70b..d16402df 100644 --- a/src/Middleware/LimitConcurrentRequestsMiddleware.php +++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -29,7 +29,8 @@ * than 10 handlers will be invoked at once: * * ```php - * $server = new StreamingServer(array( + * $server = new Server(array( + * new StreamingRequestMiddleware(), * new LimitConcurrentRequestsMiddleware(10), * $handler * )); @@ -40,7 +41,8 @@ * to limit the total number of requests that can be buffered at once: * * ```php - * $server = new StreamingServer(array( + * $server = new Server(array( + * new StreamingRequestMiddleware(), * new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request * new RequestBodyParserMiddleware(), @@ -53,7 +55,8 @@ * processes one request after another without any concurrency: * * ```php - * $server = new StreamingServer(array( + * $server = new Server(array( + * new StreamingRequestMiddleware(), * new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request * new RequestBodyParserMiddleware(), diff --git a/src/Server.php b/src/Server.php index 8d159a0a..7c4724c5 100644 --- a/src/Server.php +++ b/src/Server.php @@ -4,6 +4,7 @@ use Evenement\EventEmitter; use React\Http\Io\IniUtil; +use React\Http\Io\StreamingServer; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\StreamingRequestMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; @@ -14,15 +15,14 @@ * The `Server` class is responsible for handling incoming connections and then * processing each incoming HTTP request. * - * It buffers and parses the complete incoming HTTP request in memory. Once the - * complete request has been received, it will invoke the request handler function. - * This request handler function needs to be passed to the constructor and will be - * invoked with the respective [request](#request) object and expects a - * [response](#response) object in return: + * When a complete HTTP request has been received, it will invoke the given + * request handler function. This request handler function needs to be passed to + * the constructor and will be invoked with the respective [request](#request) + * object and expects a [response](#response) object in return: * * ```php - * $server = new Server(function (ServerRequestInterface $request) { - * return new Response( + * $server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { + * return new React\Http\Response( * 200, * array( * 'Content-Type' => 'text/plain' @@ -35,35 +35,36 @@ * Each incoming HTTP request message is always represented by the * [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), * see also following [request](#request) chapter for more details. + * * Each outgoing HTTP response message is always represented by the * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), * see also following [response](#response) chapter for more details. * - * In order to process any connections, the server needs to be attached to an - * instance of `React\Socket\ServerInterface` through the [`listen()`](#listen) method - * as described in the following chapter. In its most simple form, you can attach - * this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) - * in order to start a plaintext HTTP server like this: + * In order to start listening for any incoming connections, the `Server` needs + * to be attached to an instance of + * [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) + * through the [`listen()`](#listen) method as described in the following + * chapter. In its most simple form, you can attach this to a + * [`React\Socket\Server`](https://github.com/reactphp/socket#server) in order + * to start a plaintext HTTP server like this: * * ```php - * $server = new Server($handler); + * $server = new React\Http\Server($handler); * * $socket = new React\Socket\Server('0.0.0.0:8080', $loop); * $server->listen($socket); * ``` * - * See also the [`listen()`](#listen) method and the [first example](examples) for more details. - * - * The `Server` class is built as a facade around the underlying - * [`StreamingServer`](#streamingserver) to provide sane defaults for 80% of the - * use cases and is the recommended way to use this library unless you're sure - * you know what you're doing. + * See also the [`listen()`](#listen) method and the [first example](../examples/) + * for more details. * - * Unlike the underlying [`StreamingServer`](#streamingserver), this class - * buffers and parses the complete incoming HTTP request in memory. Once the - * complete request has been received, it will invoke the request handler - * function. This means the [request](#request) passed to your request handler - * function will be fully compatible with PSR-7. + * By default, the `Server` buffers and parses the complete incoming HTTP + * request in memory. It will invoke the given request handler function when the + * complete request headers and request body has been received. This means the + * [request](#request) object passed to your request handler function will be + * fully compatible with PSR-7 (http-message). This provides sane defaults for + * 80% of the use cases and is the recommended way to use this library unless + * you're sure you know what you're doing. * * On the other hand, buffering complete HTTP requests in memory until they can * be processed by your request handler function means that this class has to @@ -85,9 +86,9 @@ * max_file_uploads 20 * ``` * - * In particular, the `post_max_size` setting limits how much memory a single HTTP - * request is allowed to consume while buffering its request body. On top of - * this, this class will try to avoid consuming more than 1/4 of your + * In particular, the `post_max_size` setting limits how much memory a single + * HTTP request is allowed to consume while buffering its request body. On top + * of this, this class will try to avoid consuming more than 1/4 of your * `memory_limit` for buffering multiple concurrent HTTP requests. As such, with * the above default settings of `128M` max, it will try to consume no more than * `32M` for buffering multiple concurrent HTTP requests. As a consequence, it @@ -95,16 +96,52 @@ * * It is imperative that you assign reasonable values to your PHP ini settings. * It is usually recommended to either reduce the memory a single request is - * allowed to take (set `post_max_size 1M` or less) or to increase the total memory - * limit to allow for more concurrent requests (set `memory_limit 512M` or more). - * Failure to do so means that this class may have to disable concurrency and - * only handle one request at a time. - * - * Internally, this class automatically assigns these limits to the - * [middleware](#middleware) request handlers as described below. For more - * advanced use cases, you may also use the advanced - * [`StreamingServer`](#streamingserver) and assign these middleware request - * handlers yourself as described in the following chapters. + * allowed to take (set `post_max_size 1M` or less) or to increase the total + * memory limit to allow for more concurrent requests (set `memory_limit 512M` + * or more). Failure to do so means that this class may have to disable + * concurrency and only handle one request at a time. + * + * As an alternative to the above buffering defaults, you can also configure + * the `Server` explicitly to override these defaults. You can use the + * [`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and + * [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) + * to explicitly configure the total number of requests that can be handled at + * once like this: + * + * ```php + * $server = new React\Http\Server(array( + * new React\Http\Middleware\StreamingRequestMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + * new React\Http\Middleware\RequestBodyParserMiddleware(), + * $handler + * )); + * ``` + * + * > Internally, this class automatically assigns these middleware handlers + * automatically when no [`StreamingRequestMiddleware`](#streamingrequestmiddleware) + * is given. Accordingly, you can use this example to override all default + * settings to implement custom limits. + * + * As an alternative to buffering the complete request body in memory, you can + * also use a streaming approach where only small chunks of data have to be kept + * in memory: + * + * ```php + * $server = new React\Http\Server(array( + * new React\Http\Middleware\StreamingRequestMiddleware(), + * $handler + * )); + * ``` + * + * In this case, it will invoke the request handler function once the HTTP + * request headers have been received, i.e. before receiving the potentially + * much larger HTTP request body. This means the [request](#request) passed to + * your request handler function may not be fully compatible with PSR-7. This is + * specifically designed to help with more advanced use cases where you want to + * have full control over consuming the incoming HTTP request body and + * concurrency settings. See also [streaming incoming request](#streaming-incoming-request) + * below for more details. */ final class Server extends EventEmitter { @@ -119,7 +156,15 @@ final class Server extends EventEmitter private $streamingServer; /** - * @see StreamingServer::__construct() + * Creates an HTTP server that invokes the given callback for each incoming HTTP request + * + * In order to process any connections, the server needs to be attached to an + * instance of `React\Socket\ServerInterface` which emits underlying streaming + * connections in order to then parse incoming data as HTTP. + * See also [listen()](#listen) for more details. + * + * @param callable|callable[] $requestHandler + * @see self::listen() */ public function __construct($requestHandler) { @@ -167,7 +212,53 @@ public function __construct($requestHandler) } /** - * @see StreamingServer::listen() + * Starts listening for HTTP requests on the given socket server instance + * + * The given [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) + * is responsible for emitting the underlying streaming connections. This + * HTTP server needs to be attached to it in order to process any + * connections and pase incoming streaming data as incoming HTTP request + * messages. In its most common form, you can attach this to a + * [`React\Socket\Server`](https://github.com/reactphp/socket#server) in + * order to start a plaintext HTTP server like this: + * + * ```php + * $server = new React\Http\Server($handler); + * + * $socket = new React\Socket\Server(8080, $loop); + * $server->listen($socket); + * ``` + * + * See also [example #1](examples) for more details. + * + * This example will start listening for HTTP requests on the alternative + * HTTP port `8080` on all interfaces (publicly). As an alternative, it is + * very common to use a reverse proxy and let this HTTP server listen on the + * localhost (loopback) interface only by using the listen address + * `127.0.0.1:8080` instead. This way, you host your application(s) on the + * default HTTP port `80` and only route specific requests to this HTTP + * server. + * + * Likewise, it's usually recommended to use a reverse proxy setup to accept + * secure HTTPS requests on default HTTPS port `443` (TLS termination) and + * only route plaintext requests to this HTTP server. As an alternative, you + * can also accept secure HTTPS requests with this HTTP server by attaching + * this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) + * using a secure TLS listen address, a certificate file and optional + * `passphrase` like this: + * + * ```php + * $server = new React\Http\Server($handler); + * + * $socket = new React\Socket\Server('tls://0.0.0.0:8443', $loop, array( + * 'local_cert' => __DIR__ . '/localhost.pem' + * )); + * $server->listen($socket); + * ``` + * + * See also [example #11](examples) for more details. + * + * @param ServerInterface $socket */ public function listen(ServerInterface $server) { diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index e0feb476..2de43ce8 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -6,7 +6,7 @@ use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Response; -use React\Http\StreamingServer; +use React\Http\Server; use React\Socket\Server as Socket; use React\EventLoop\Factory; use Psr\Http\Message\RequestInterface; @@ -17,6 +17,7 @@ use React\Promise; use React\Promise\Stream; use React\Stream\ThroughStream; +use React\Http\Middleware\StreamingRequestMiddleware; class FunctionalServerTest extends TestCase { @@ -25,7 +26,7 @@ public function testPlainHttpOnRandomPort() $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -51,7 +52,7 @@ public function testPlainHttpOnRandomPortWithSingleRequestHandlerArray() $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(array( + $server = new Server(array( function () { return new Response(404); }, @@ -78,7 +79,7 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -104,7 +105,7 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -136,7 +137,7 @@ public function testSecureHttpsOnRandomPort() 'tls' => array('verify_peer' => false) )); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -168,7 +169,7 @@ public function testSecureHttpsReturnsData() $loop = Factory::create(); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response( 200, array(), @@ -212,7 +213,7 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() 'tls' => array('verify_peer' => false) )); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -246,7 +247,7 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() } $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -276,7 +277,7 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort } $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -315,7 +316,7 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() 'tls' => array('verify_peer' => false) )); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -354,7 +355,7 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() 'tls' => array('verify_peer' => false) )); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -384,7 +385,7 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() } $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -423,7 +424,7 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() 'tls' => array('verify_peer' => false) )); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri() . 'x' . $request->getHeaderLine('Host')); }); @@ -451,7 +452,7 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() $stream = new ThroughStream(); $stream->close(); - $server = new StreamingServer(function (RequestInterface $request) use ($stream) { + $server = new Server(function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); @@ -472,15 +473,18 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() $socket->close(); } - public function testRequestHandlerWillReceiveCloseEventIfConnectionClosesWhileSendingBody() + public function testRequestHandlerWithStreamingRequestWillReceiveCloseEventIfConnectionClosesWhileSendingBody() { $loop = Factory::create(); $connector = new Connector($loop); $once = $this->expectCallableOnce(); - $server = new StreamingServer(function (RequestInterface $request) use ($once) { - $request->getBody()->on('close', $once); - }); + $server = new Server(array( + new StreamingRequestMiddleware(), + function (RequestInterface $request) use ($once) { + $request->getBody()->on('close', $once); + } + )); $socket = new Socket(0, $loop); $server->listen($socket); @@ -498,16 +502,19 @@ public function testRequestHandlerWillReceiveCloseEventIfConnectionClosesWhileSe $socket->close(); } - public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileSendingRequestBody() + public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileSendingStreamingRequestBody() { $loop = Factory::create(); $connector = new Connector($loop); $stream = new ThroughStream(); - $server = new StreamingServer(function (RequestInterface $request) use ($stream) { - return new Response(200, array(), $stream); - }); + $server = new Server(array( + new StreamingRequestMiddleware(), + function (RequestInterface $request) use ($stream) { + return new Response(200, array(), $stream); + } + )); $socket = new Socket(0, $loop); $server->listen($socket); @@ -535,7 +542,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() $stream = new ThroughStream(); - $server = new StreamingServer(function (RequestInterface $request) use ($stream) { + $server = new Server(function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); @@ -563,7 +570,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) use ($loop) { + $server = new Server(function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -600,7 +607,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) use ($loop) { + $server = new Server(function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -638,7 +645,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) use ($loop) { + $server = new Server(function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -675,7 +682,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) use ($loop) { + $server = new Server(function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -716,7 +723,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(function (RequestInterface $request) { + $server = new Server(function (RequestInterface $request) { $stream = new ThroughStream(); $stream->close(); @@ -750,7 +757,7 @@ public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing() $loop = Factory::create(); $connector = new Connector($loop); - $server = new StreamingServer(array( + $server = new Server(array( new LimitConcurrentRequestsMiddleware(5), new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (ServerRequestInterface $request, $next) use ($loop) { diff --git a/tests/StreamingServerTest.php b/tests/Io/StreamingServerTest.php similarity index 99% rename from tests/StreamingServerTest.php rename to tests/Io/StreamingServerTest.php index b620061a..35396f79 100644 --- a/tests/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -1,12 +1,14 @@ Date: Tue, 7 Jul 2020 09:04:20 +0200 Subject: [PATCH 012/152] Prepare TOC to avoid name collisions with HTTP client --- README.md | 12 ++++++------ src/Server.php | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d53e1540..84af8325 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Usage](#usage) * [Server](#server) * [listen()](#listen) - * [Request](#request) + * [Server Request](#server-request) * [Request parameters](#request-parameters) * [Query parameters](#query-parameters) * [Request body](#request-body) @@ -71,7 +71,7 @@ processing each incoming HTTP request. When a complete HTTP request has been received, it will invoke the given request handler function. This request handler function needs to be passed to -the constructor and will be invoked with the respective [request](#request) +the constructor and will be invoked with the respective [request](#server-request) object and expects a [response](#response) object in return: ```php @@ -88,7 +88,7 @@ $server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterfac Each incoming HTTP request message is always represented by the [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), -see also following [request](#request) chapter for more details. +see also following [request](#server-request) chapter for more details. Each outgoing HTTP response message is always represented by the [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), @@ -115,7 +115,7 @@ for more details. By default, the `Server` buffers and parses the complete incoming HTTP request in memory. It will invoke the given request handler function when the complete request headers and request body has been received. This means the -[request](#request) object passed to your request handler function will be +[request](#server-request) object passed to your request handler function will be fully compatible with PSR-7 (http-message). This provides sane defaults for 80% of the use cases and is the recommended way to use this library unless you're sure you know what you're doing. @@ -190,7 +190,7 @@ $server = new React\Http\Server(array( In this case, it will invoke the request handler function once the HTTP request headers have been received, i.e. before receiving the potentially -much larger HTTP request body. This means the [request](#request) passed to +much larger HTTP request body. This means the [request](#server-request) passed to your request handler function may not be fully compatible with PSR-7. This is specifically designed to help with more advanced use cases where you want to have full control over consuming the incoming HTTP request body and @@ -246,7 +246,7 @@ $server->listen($socket); See also [example #11](examples) for more details. -### Request +### Server Request As seen above, the [`Server`](#server) class is responsible for handling incoming connections and then processing each incoming HTTP request. diff --git a/src/Server.php b/src/Server.php index 7c4724c5..3fe942c9 100644 --- a/src/Server.php +++ b/src/Server.php @@ -17,7 +17,7 @@ * * When a complete HTTP request has been received, it will invoke the given * request handler function. This request handler function needs to be passed to - * the constructor and will be invoked with the respective [request](#request) + * the constructor and will be invoked with the respective [request](#server-request) * object and expects a [response](#response) object in return: * * ```php @@ -34,7 +34,7 @@ * * Each incoming HTTP request message is always represented by the * [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), - * see also following [request](#request) chapter for more details. + * see also following [request](#server-request) chapter for more details. * * Each outgoing HTTP response message is always represented by the * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), @@ -61,7 +61,7 @@ * By default, the `Server` buffers and parses the complete incoming HTTP * request in memory. It will invoke the given request handler function when the * complete request headers and request body has been received. This means the - * [request](#request) object passed to your request handler function will be + * [request](#server-request) object passed to your request handler function will be * fully compatible with PSR-7 (http-message). This provides sane defaults for * 80% of the use cases and is the recommended way to use this library unless * you're sure you know what you're doing. @@ -136,7 +136,7 @@ * * In this case, it will invoke the request handler function once the HTTP * request headers have been received, i.e. before receiving the potentially - * much larger HTTP request body. This means the [request](#request) passed to + * much larger HTTP request body. This means the [request](#server-request) passed to * your request handler function may not be fully compatible with PSR-7. This is * specifically designed to help with more advanced use cases where you want to * have full control over consuming the incoming HTTP request body and From 2538e61eead1b438d73f4362dc2d819d1318cfb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 3 Jul 2020 22:02:52 +0200 Subject: [PATCH 013/152] Import clue/reactphp-buzz v2.9.0 Change namespace from `Clue\React\Buzz` to `React\Http` and update all tests with merged namespaces. See https://github.com/clue/reactphp-buzz for original repo. --- LICENSE | 3 + README.md | 1328 +++++++++++++++++++++- composer.json | 16 +- examples/01-google.php | 15 + examples/02-concurrent.php | 23 + examples/03-any.php | 32 + examples/04-post-json.php | 29 + examples/05-put-xml.php | 26 + examples/11-http-proxy.php | 29 + examples/12-socks-proxy.php | 29 + examples/13-ssh-proxy.php | 29 + examples/14-unix-domain-sockets.php | 27 + examples/21-stream-forwarding.php | 33 + examples/22-stream-stdin.php | 27 + examples/91-benchmark-download.php | 61 + examples/92-benchmark-upload.php | 125 ++ src/Browser.php | 867 ++++++++++++++ src/Io/ChunkedEncoder.php | 30 +- src/Io/Sender.php | 161 +++ src/Io/Transaction.php | 305 +++++ src/Message/MessageFactory.php | 139 +++ src/Message/ReadableBodyStream.php | 153 +++ src/Message/ResponseException.php | 43 + tests/BrowserTest.php | 414 +++++++ tests/FunctionalBrowserTest.php | 645 +++++++++++ tests/Io/ChunkedEncoderTest.php | 4 +- tests/Io/SenderTest.php | 393 +++++++ tests/Io/TransactionTest.php | 861 ++++++++++++++ tests/Message/MessageFactoryTest.php | 197 ++++ tests/Message/ReadableBodyStreamTest.php | 255 +++++ tests/Message/ResponseExceptionTest.php | 23 + tests/TestCase.php | 14 +- 32 files changed, 6291 insertions(+), 45 deletions(-) create mode 100644 examples/01-google.php create mode 100644 examples/02-concurrent.php create mode 100644 examples/03-any.php create mode 100644 examples/04-post-json.php create mode 100644 examples/05-put-xml.php create mode 100644 examples/11-http-proxy.php create mode 100644 examples/12-socks-proxy.php create mode 100644 examples/13-ssh-proxy.php create mode 100644 examples/14-unix-domain-sockets.php create mode 100644 examples/21-stream-forwarding.php create mode 100644 examples/22-stream-stdin.php create mode 100644 examples/91-benchmark-download.php create mode 100644 examples/92-benchmark-upload.php create mode 100644 src/Browser.php create mode 100644 src/Io/Sender.php create mode 100644 src/Io/Transaction.php create mode 100644 src/Message/MessageFactory.php create mode 100644 src/Message/ReadableBodyStream.php create mode 100644 src/Message/ResponseException.php create mode 100644 tests/BrowserTest.php create mode 100644 tests/FunctionalBrowserTest.php create mode 100644 tests/Io/SenderTest.php create mode 100644 tests/Io/TransactionTest.php create mode 100644 tests/Message/MessageFactoryTest.php create mode 100644 tests/Message/ReadableBodyStreamTest.php create mode 100644 tests/Message/ResponseExceptionTest.php diff --git a/LICENSE b/LICENSE index a808108c..0ca9208a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,6 @@ +The MIT License (MIT) + +Copyright (c) 2013 Christian Lück Copyright (c) 2012 Igor Wiedler, Chris Boden Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/README.md b/README.md index 84af8325..af2fada9 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,22 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht **Table of contents** * [Quickstart example](#quickstart-example) -* [Usage](#usage) +* [Client Usage](#client-usage) + * [Request methods](#request-methods) + * [Promises](#promises) + * [Cancellation](#cancellation) + * [Timeouts](#timeouts) + * [Authentication](#authentication) + * [Redirects](#redirects) + * [Blocking](#blocking) + * [Concurrency](#concurrency) + * [Streaming response](#streaming-response) + * [Streaming request](#streaming-request) + * [HTTP proxy](#http-proxy) + * [SOCKS proxy](#socks-proxy) + * [SSH proxy](#ssh-proxy) + * [Unix domain sockets](#unix-domain-sockets) +* [Server Usage](#server-usage) * [Server](#server) * [listen()](#listen) * [Server Request](#server-request) @@ -28,17 +43,54 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [Custom middleware](#custom-middleware) * [Third-Party Middleware](#third-party-middleware) * [API](#api) + * [Browser](#browser) + * [get()](#get) + * [post()](#post) + * [head()](#head) + * [patch()](#patch) + * [put()](#put) + * [delete()](#delete) + * [request()](#request) + * [requestStreaming()](#requeststreaming) + * [~~submit()~~](#submit) + * [~~send()~~](#send) + * [withTimeout()](#withtimeout) + * [withFollowRedirects()](#withfollowredirects) + * [withRejectErrorResponse()](#withrejecterrorresponse) + * [withBase()](#withbase) + * [withProtocolVersion()](#withprotocolversion) + * [withResponseBuffer()](#withresponsebuffer) + * [~~withOptions()~~](#withoptions) + * [~~withoutBase()~~](#withoutbase) * [React\Http\Middleware](#reacthttpmiddleware) * [StreamingRequestMiddleware](#streamingrequestmiddleware) * [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) * [RequestBodyBufferMiddleware](#requestbodybuffermiddleware) * [RequestBodyParserMiddleware](#requestbodyparsermiddleware) + * [ResponseInterface](#responseinterface) + * [RequestInterface](#requestinterface) + * [UriInterface](#uriinterface) + * [ResponseException](#responseexception) * [Install](#install) * [Tests](#tests) * [License](#license) ## Quickstart example +Once [installed](#install), you can use the following code to access a +HTTP webserver and send some simple HTTP GET requests: + +```php +$loop = React\EventLoop\Factory::create(); +$client = new React\Http\Browser($loop); + +$client->get('http://www.google.com/')->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump($response->getHeaders(), (string)$response->getBody()); +}); + +$loop->run(); +``` + This is an HTTP server which responds with `Hello World!` to every request. ```php @@ -62,7 +114,597 @@ $loop->run(); See also the [examples](examples). -## Usage +## Client Usage + +### Request methods + + + +Most importantly, this project provides a [`Browser`](#browser) object that +offers several methods that resemble the HTTP protocol methods: + +```php +$browser->get($url, array $headers = array()); +$browser->head($url, array $headers = array()); +$browser->post($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); +$browser->delete($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); +$browser->put($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); +$browser->patch($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); +``` + +Each of these methods requires a `$url` and some optional parameters to send an +HTTP request. Each of these method names matches the respective HTTP request +method, for example the [`get()`](#get) method sends an HTTP `GET` request. + +You can optionally pass an associative array of additional `$headers` that will be +sent with this HTTP request. Additionally, each method will automatically add a +matching `Content-Length` request header if an outgoing request body is given and its +size is known and non-empty. For an empty request body, if will only include a +`Content-Length: 0` request header if the request method usually expects a request +body (only applies to `POST`, `PUT` and `PATCH` HTTP request methods). + +If you're using a [streaming request body](#streaming-request), it will default +to using `Transfer-Encoding: chunked` unless you explicitly pass in a matching `Content-Length` +request header. See also [streaming request](#streaming-request) for more details. + +By default, all of the above methods default to sending requests using the +HTTP/1.1 protocol version. If you want to explicitly use the legacy HTTP/1.0 +protocol version, you can use the [`withProtocolVersion()`](#withprotocolversion) +method. If you want to use any other or even custom HTTP request method, you can +use the [`request()`](#request) method. + +Each of the above methods supports async operation and either *fulfills* with a +[`ResponseInterface`](#responseinterface) or *rejects* with an `Exception`. +Please see the following chapter about [promises](#promises) for more details. + +### Promises + +Sending requests is async (non-blocking), so you can actually send multiple +requests in parallel. +The `Browser` will respond to each request with a [`ResponseInterface`](#responseinterface) +message, the order is not guaranteed. +Sending requests uses a [Promise](https://github.com/reactphp/promise)-based +interface that makes it easy to react to when an HTTP request is completed +(i.e. either successfully fulfilled or rejected with an error): + +```php +$browser->get($url)->then( + function (Psr\Http\Message\ResponseInterface $response) { + var_dump('Response received', $response); + }, + function (Exception $error) { + var_dump('There was an error', $error->getMessage()); + } +); +``` + +If this looks strange to you, you can also use the more traditional [blocking API](#blocking). + +Keep in mind that resolving the Promise with the full response message means the +whole response body has to be kept in memory. +This is easy to get started and works reasonably well for smaller responses +(such as common HTML pages or RESTful or JSON API requests). + +You may also want to look into the [streaming API](#streaming-response): + +* If you're dealing with lots of concurrent requests (100+) or +* If you want to process individual data chunks as they happen (without having to wait for the full response body) or +* If you're expecting a big response body size (1 MiB or more, for example when downloading binary files) or +* If you're unsure about the response body size (better be safe than sorry when accessing arbitrary remote HTTP endpoints and the response body size is unknown in advance). + +### Cancellation + +The returned Promise is implemented in such a way that it can be cancelled +when it is still pending. +Cancelling a pending promise will reject its value with an Exception and +clean up any underlying resources. + +```php +$promise = $browser->get($url); + +$loop->addTimer(2.0, function () use ($promise) { + $promise->cancel(); +}); +``` + +### Timeouts + +This library uses a very efficient HTTP implementation, so most HTTP requests +should usually be completed in mere milliseconds. However, when sending HTTP +requests over an unreliable network (the internet), there are a number of things +that can go wrong and may cause the request to fail after a time. As such, this +library respects PHP's `default_socket_timeout` setting (default 60s) as a timeout +for sending the outgoing HTTP request and waiting for a successful response and +will otherwise cancel the pending request and reject its value with an Exception. + +Note that this timeout value covers creating the underlying transport connection, +sending the HTTP request, receiving the HTTP response headers and its full +response body and following any eventual [redirects](#redirects). See also +[redirects](#redirects) below to configure the number of redirects to follow (or +disable following redirects altogether) and also [streaming](#streaming-response) +below to not take receiving large response bodies into account for this timeout. + +You can use the [`withTimeout()` method](#withtimeout) to pass a custom timeout +value in seconds like this: + +```php +$browser = $browser->withTimeout(10.0); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // response received within 10 seconds maximum + var_dump($response->getHeaders()); +}); +``` + +Similarly, you can use a bool `false` to not apply a timeout at all +or use a bool `true` value to restore the default handling. +See [`withTimeout()`](#withtimeout) for more details. + +If you're using a [streaming response body](#streaming-response), the time it +takes to receive the response body stream will not be included in the timeout. +This allows you to keep this incoming stream open for a longer time, such as +when downloading a very large stream or when streaming data over a long-lived +connection. + +If you're using a [streaming request body](#streaming-request), the time it +takes to send the request body stream will not be included in the timeout. This +allows you to keep this outgoing stream open for a longer time, such as when +uploading a very large stream. + +Note that this timeout handling applies to the higher-level HTTP layer. Lower +layers such as socket and DNS may also apply (different) timeout values. In +particular, the underlying socket connection uses the same `default_socket_timeout` +setting to establish the underlying transport connection. To control this +connection timeout behavior, you can [inject a custom `Connector`](#browser) +like this: + +```php +$browser = new React\Http\Browser( + $loop, + new React\Socket\Connector( + $loop, + array( + 'timeout' => 5 + ) + ) +); +``` + +### Authentication + +This library supports [HTTP Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) +using the `Authorization: Basic …` request header or allows you to set an explicit +`Authorization` request header. + +By default, this library does not include an outgoing `Authorization` request +header. If the server requires authentication, if may return a `401` (Unauthorized) +status code which will reject the request by default (see also the +[`withRejectErrorResponse()` method](#withrejecterrorresponse) below). + +In order to pass authentication details, you can simple pass the username and +password as part of the request URL like this: + +```php +$promise = $browser->get('https://user:pass@example.com/api'); +``` + +Note that special characters in the authentication details have to be +percent-encoded, see also [`rawurlencode()`](https://www.php.net/manual/en/function.rawurlencode.php). +This example will automatically pass the base64-encoded authentication details +using the outgoing `Authorization: Basic …` request header. If the HTTP endpoint +you're talking to requires any other authentication scheme, you can also pass +this header explicitly. This is common when using (RESTful) HTTP APIs that use +OAuth access tokens or JSON Web Tokens (JWT): + +```php +$token = 'abc123'; + +$promise = $browser->get( + 'https://example.com/api', + array( + 'Authorization' => 'Bearer ' . $token + ) +); +``` + +When following redirects, the `Authorization` request header will never be sent +to any remote hosts by default. When following a redirect where the `Location` +response header contains authentication details, these details will be sent for +following requests. See also [redirects](#redirects) below. + +### Redirects + +By default, this library follows any redirects and obeys `3xx` (Redirection) +status codes using the `Location` response header from the remote server. +The promise will be fulfilled with the last response from the chain of redirects. + +```php +$browser->get($url, $headers)->then(function (Psr\Http\Message\ResponseInterface $response) { + // the final response will end up here + var_dump($response->getHeaders()); +}); +``` + +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 used HTTP authentication with an `Authorization` request +header, this request header will only be passed as part of the redirected +request if the redirected URL is using the same host. In other words, the +`Authorizaton` request header will not be forwarded to other foreign hosts due to +possible privacy/security concerns. When following a redirect where the `Location` +response header contains authentication details, these details will be sent for +following requests. + +You can use the [`withFollowRedirects()`](#withfollowredirects) method to +control the maximum number of redirects to follow or to return any redirect +responses as-is and apply custom redirection logic like this: + +```php +$browser = $browser->withFollowRedirects(false); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // any redirects will now end up here + var_dump($response->getHeaders()); +}); +``` + +See also [`withFollowRedirects()`](#withfollowredirects) for more details. + +### Blocking + +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: + +```php +use Clue\React\Block; + +$loop = React\EventLoop\Factory::create(); +$browser = new React\Http\Browser($loop); + +$promise = $browser->get('http://example.com/'); + +try { + $response = Block\await($promise, $loop); + // response successfully received +} catch (Exception $e) { + // an error occured while performing the request +} +``` + +Similarly, you can also process multiple requests concurrently and await an array of `Response` objects: + +```php +$promises = array( + $browser->get('http://example.com/'), + $browser->get('http://www.example.org/'), +); + +$responses = Block\awaitAll($promises, $loop); +``` + +Please refer to [clue/reactphp-block](https://github.com/clue/reactphp-block#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 +[streaming API](#streaming-response). + +### Concurrency + +As stated above, this library provides you a powerful, async API. Being able to +send a large number of requests at once is one of the core features of this +project. For instance, you can easily send 100 requests concurrently while +processing SQL queries at the same time. + +Remember, with great power comes great responsibility. Sending an excessive +number of requests may either take up all resources on your side or it may even +get you banned by the remote side if it sees an unreasonable number of requests +from your side. + +```php +// watch out if array contains many elements +foreach ($urls as $url) { + $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump($response->getHeaders()); + }); +} +``` + +As a consequence, it's usually recommended to limit concurrency on the sending +side to a reasonable value. It's common to use a rather small limit, as doing +more than a dozen of things at once may easily overwhelm the receiving side. You +can use [clue/reactphp-mq](https://github.com/clue/reactphp-mq) as a lightweight +in-memory queue to concurrently do many (but not too many) things at once: + +```php +// wraps Browser in a Queue object that executes no more than 10 operations at once +$q = new Clue\React\Mq\Queue(10, null, function ($url) use ($browser) { + return $browser->get($url); +}); + +foreach ($urls as $url) { + $q($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump($response->getHeaders()); + }); +} +``` + +Additional requests that exceed the concurrency limit will automatically be +enqueued until one of the pending requests completes. This integrates nicely +with the existing [Promise-based API](#promises). Please refer to +[clue/reactphp-mq](https://github.com/clue/reactphp-mq) for more details. + +This in-memory approach works reasonably well for some thousand outstanding +requests. If you're processing a very large input list (think millions of rows +in a CSV or NDJSON file), you may want to look into using a streaming approach +instead. See [clue/reactphp-flux](https://github.com/clue/reactphp-flux) for +more details. + +### Streaming response + + + +All of the above examples assume you want to store the whole response body in memory. +This is easy to get started and works reasonably well for smaller responses. + +However, there are several situations where it's usually a better idea to use a +streaming approach, where only small chunks have to be kept in memory: + +* If you're dealing with lots of concurrent requests (100+) or +* If you want to process individual data chunks as they happen (without having to wait for the full response body) or +* If you're expecting a big response body size (1 MiB or more, for example when downloading binary files) or +* If you're unsure about the response body size (better be safe than sorry when accessing arbitrary remote HTTP endpoints and the response body size is unknown in advance). + +You can use the [`requestStreaming()`](#requeststreaming) method to send an +arbitrary HTTP request and receive a streaming response. It uses the same HTTP +message API, but does not buffer the response body in memory. It only processes +the response body in small chunks as data is received and forwards this data +through [ReactPHP's Stream API](https://github.com/reactphp/stream). This works +for (any number of) responses of arbitrary sizes. + +This means it resolves with a normal [`ResponseInterface`](#responseinterface), +which can be used to access the response message parameters as usual. +You can access the message body as usual, however it now also +implements ReactPHP's [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +as well as parts of the PSR-7's [`StreamInterface`](https://www.php-fig.org/psr/psr-7/#3-4-psr-http-message-streaminterface). + +```php +$browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + $body = $response->getBody(); + assert($body instanceof Psr\Http\Message\StreamInterface); + assert($body instanceof React\Stream\ReadableStreamInterface); + + $body->on('data', function ($chunk) { + echo $chunk; + }); + + $body->on('error', function (Exception $error) { + echo 'Error: ' . $error->getMessage() . PHP_EOL; + }); + + $body->on('close', function () { + echo '[DONE]' . PHP_EOL; + }); +}); +``` + +See also the [stream download example](examples/91-benchmark-download.php) and +the [stream forwarding example](examples/21-stream-forwarding.php). + +You can invoke the following methods on the message body: + +```php +$body->on($event, $callback); +$body->eof(); +$body->isReadable(); +$body->pipe(React\Stream\WritableStreamInterface $dest, array $options = array()); +$body->close(); +$body->pause(); +$body->resume(); +``` + +Because the message body is in a streaming state, invoking the following methods +doesn't make much sense: + +```php +$body->__toString(); // '' +$body->detach(); // throws BadMethodCallException +$body->getSize(); // null +$body->tell(); // throws BadMethodCallException +$body->isSeekable(); // false +$body->seek(); // throws BadMethodCallException +$body->rewind(); // throws BadMethodCallException +$body->isWritable(); // false +$body->write(); // throws BadMethodCallException +$body->read(); // throws BadMethodCallException +$body->getContents(); // throws BadMethodCallException +``` + +Note how [timeouts](#timeouts) apply slightly differently when using streaming. +In streaming mode, the timeout value covers creating the underlying transport +connection, sending the HTTP request, receiving the HTTP response headers and +following any eventual [redirects](#redirects). In particular, the timeout value +does not take receiving (possibly large) response bodies into account. + +If you want to integrate the streaming response into a higher level API, then +working with Promise objects that resolve with Stream objects is often inconvenient. +Consider looking into also using [react/promise-stream](https://github.com/reactphp/promise-stream). +The resulting streaming code could look something like this: + +```php +use React\Promise\Stream; + +function download(Browser $browser, string $url): React\Stream\ReadableStreamInterface { + return Stream\unwrapReadable( + $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + return $response->getBody(); + }) + ); +} + +$stream = download($browser, $url); +$stream->on('data', function ($data) { + echo $data; +}); +``` + +See also the [`requestStreaming()`](#requeststreaming) method for more details. + +> Legacy info: Legacy versions prior to v2.9.0 used the legacy + [`streaming` option](#withoptions). This option is now deprecated but otherwise + continues to show the exact same behavior. + +### Streaming request + +Besides streaming the response body, you can also stream the request body. +This can be useful if you want to send big POST requests (uploading files etc.) +or process many outgoing streams at once. +Instead of passing the body as a string, you can simply pass an instance +implementing ReactPHP's [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +to the [request methods](#request-methods) like this: + +```php +$browser->post($url, array(), $stream)->then(function (Psr\Http\Message\ResponseInterface $response) { + echo 'Successfully sent.'; +}); +``` + +If you're using a streaming request body (`React\Stream\ReadableStreamInterface`), it will +default to using `Transfer-Encoding: chunked` or you have to explicitly pass in a +matching `Content-Length` request header like so: + +```php +$body = new React\Stream\ThroughStream(); +$loop->addTimer(1.0, function () use ($body) { + $body->end("hello world"); +}); + +$browser->post($url, array('Content-Length' => '11'), $body); +``` + +If the streaming request body emits an `error` event or is explicitly closed +without emitting a successful `end` event first, the request will automatically +be closed and rejected. + +### HTTP proxy + +You can also establish your outgoing connections through an HTTP CONNECT proxy server +by adding a dependency to [clue/reactphp-http-proxy](https://github.com/clue/reactphp-http-proxy). + +HTTP CONNECT proxy servers (also commonly known as "HTTPS proxy" or "SSL proxy") +are commonly used to tunnel HTTPS traffic through an intermediary ("proxy"), to +conceal the origin address (anonymity) or to circumvent address blocking +(geoblocking). While many (public) HTTP CONNECT proxy servers often limit this +to HTTPS port`443` only, this can technically be used to tunnel any TCP/IP-based +protocol, such as plain HTTP and TLS-encrypted HTTPS. + +```php +$proxy = new Clue\React\HttpProxy\ProxyConnector( + 'http://127.0.0.1:8080/', + new React\Socket\Connector($loop) +); + +$connector = new React\Socket\Connector($loop, array( + 'tcp' => $proxy, + 'dns' => false +)); + +$browser = new React\Http\Browser($loop, $connector); +``` + +See also the [HTTP CONNECT proxy example](examples/11-http-proxy.php). + +### SOCKS proxy + +You can also establish your outgoing connections through a SOCKS proxy server +by adding a dependency to [clue/reactphp-socks](https://github.com/clue/reactphp-socks). + +The SOCKS proxy protocol family (SOCKS5, SOCKS4 and SOCKS4a) is commonly used to +tunnel HTTP(S) traffic through an intermediary ("proxy"), to conceal the origin +address (anonymity) or to circumvent address blocking (geoblocking). While many +(public) SOCKS proxy servers often limit this to HTTP(S) port `80` and `443` +only, this can technically be used to tunnel any TCP/IP-based protocol. + +```php +$proxy = new Clue\React\Socks\Client( + 'socks://127.0.0.1:1080', + new React\Socket\Connector($loop) +); + +$connector = new React\Socket\Connector($loop, array( + 'tcp' => $proxy, + 'dns' => false +)); + +$browser = new React\Http\Browser($loop, $connector); +``` + +See also the [SOCKS proxy example](examples/12-socks-proxy.php). + +### SSH proxy + +You can also establish your outgoing connections through an SSH server +by adding a dependency to [clue/reactphp-ssh-proxy](https://github.com/clue/reactphp-ssh-proxy). + +[Secure Shell (SSH)](https://en.wikipedia.org/wiki/Secure_Shell) is a secure +network protocol that is most commonly used to access a login shell on a remote +server. Its architecture allows it to use multiple secure channels over a single +connection. Among others, this can also be used to create an "SSH tunnel", which +is commonly used to tunnel HTTP(S) traffic through an intermediary ("proxy"), to +conceal the origin address (anonymity) or to circumvent address blocking +(geoblocking). This can be used to tunnel any TCP/IP-based protocol (HTTP, SMTP, +IMAP etc.), allows you to access local services that are otherwise not accessible +from the outside (database behind firewall) and as such can also be used for +plain HTTP and TLS-encrypted HTTPS. + +```php +$proxy = new Clue\React\SshProxy\SshSocksConnector('me@localhost:22', $loop); + +$connector = new React\Socket\Connector($loop, array( + 'tcp' => $proxy, + 'dns' => false +)); + +$browser = new React\Http\Browser($loop, $connector); +``` + +See also the [SSH proxy example](examples/13-ssh-proxy.php). + +### Unix domain sockets + +By default, this library supports transport over plaintext TCP/IP and secure +TLS connections for the `http://` and `https://` URL 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 URL so that the hostname given in the request URL will +no longer be used to establish the connection: + +```php +$connector = new React\Socket\FixedUriConnector( + 'unix:///var/run/docker.sock', + new React\Socket\UnixConnector($loop) +); + +$browser = new Browser($loop, $connector); + +$client->get('http://localhost/info')->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump($response->getHeaders(), (string)$response->getBody()); +}); +``` + +See also the [Unix Domain Sockets (UDS) example](examples/14-unix-domain-sockets.php). + + +## Server Usage ### Server @@ -1184,6 +1826,643 @@ feel free to add it to this list. ## API +### Browser + +The `React\Http\Browser` is responsible for sending HTTP requests to your HTTP server +and keeps track of pending incoming HTTP responses. +It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage). + +```php +$loop = React\EventLoop\Factory::create(); + +$browser = new React\Http\Browser($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 + ) +)); + +$browser = new React\Http\Browser($loop, $connector); +``` + +#### get() + +The `get(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to +send an HTTP GET request. + +```php +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump((string)$response->getBody()); +}); +``` + +See also [example 01](examples/01-google.php). + +> For BC reasons, this method accepts the `$url` as either a `string` + value or as an `UriInterface`. It's recommended to explicitly cast any + objects implementing `UriInterface` to `string`. + +#### post() + +The `post(string|UriInterface $url, array $headers = array(), string|ReadableStreamInterface $contents = ''): PromiseInterface` method can be used to +send an HTTP POST request. + +```php +$browser->post( + $url, + [ + 'Content-Type' => 'application/json' + ], + json_encode($data) +)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump(json_decode((string)$response->getBody())); +}); +``` + +See also [example 04](examples/04-post-json.php). + +This method is also commonly used to submit HTML form data: + +```php +$data = [ + 'user' => 'Alice', + 'password' => 'secret' +]; + +$browser->post( + $url, + [ + 'Content-Type' => 'application/x-www-form-urlencoded' + ], + http_build_query($data) +); +``` + +This method will automatically add a matching `Content-Length` request +header if the outgoing request body is a `string`. If you're using a +streaming request body (`ReadableStreamInterface`), it will default to +using `Transfer-Encoding: chunked` or you have to explicitly pass in a +matching `Content-Length` request header like so: + +```php +$body = new React\Stream\ThroughStream(); +$loop->addTimer(1.0, function () use ($body) { + $body->end("hello world"); +}); + +$browser->post($url, array('Content-Length' => '11'), $body); +``` + +> For BC reasons, this method accepts the `$url` as either a `string` + value or as an `UriInterface`. It's recommended to explicitly cast any + objects implementing `UriInterface` to `string`. + +#### head() + +The `head(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to +send an HTTP HEAD request. + +```php +$browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump($response->getHeaders()); +}); +``` + +> For BC reasons, this method accepts the `$url` as either a `string` + value or as an `UriInterface`. It's recommended to explicitly cast any + objects implementing `UriInterface` to `string`. + +#### patch() + +The `patch(string|UriInterface $url, array $headers = array(), string|ReadableStreamInterface $contents = ''): PromiseInterface` method can be used to +send an HTTP PATCH request. + +```php +$browser->patch( + $url, + [ + 'Content-Type' => 'application/json' + ], + json_encode($data) +)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump(json_decode((string)$response->getBody())); +}); +``` + +This method will automatically add a matching `Content-Length` request +header if the outgoing request body is a `string`. If you're using a +streaming request body (`ReadableStreamInterface`), it will default to +using `Transfer-Encoding: chunked` or you have to explicitly pass in a +matching `Content-Length` request header like so: + +```php +$body = new React\Stream\ThroughStream(); +$loop->addTimer(1.0, function () use ($body) { + $body->end("hello world"); +}); + +$browser->patch($url, array('Content-Length' => '11'), $body); +``` + +> For BC reasons, this method accepts the `$url` as either a `string` + value or as an `UriInterface`. It's recommended to explicitly cast any + objects implementing `UriInterface` to `string`. + +#### put() + +The `put(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to +send an HTTP PUT request. + +```php +$browser->put( + $url, + [ + 'Content-Type' => 'text/xml' + ], + $xml->asXML() +)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump((string)$response->getBody()); +}); +``` + +See also [example 05](examples/05-put-xml.php). + +This method will automatically add a matching `Content-Length` request +header if the outgoing request body is a `string`. If you're using a +streaming request body (`ReadableStreamInterface`), it will default to +using `Transfer-Encoding: chunked` or you have to explicitly pass in a +matching `Content-Length` request header like so: + +```php +$body = new React\Stream\ThroughStream(); +$loop->addTimer(1.0, function () use ($body) { + $body->end("hello world"); +}); + +$browser->put($url, array('Content-Length' => '11'), $body); +``` + +> For BC reasons, this method accepts the `$url` as either a `string` + value or as an `UriInterface`. It's recommended to explicitly cast any + objects implementing `UriInterface` to `string`. + +#### delete() + +The `delete(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to +send an HTTP DELETE request. + +```php +$browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump((string)$response->getBody()); +}); +``` + +> For BC reasons, this method accepts the `$url` as either a `string` + value or as an `UriInterface`. It's recommended to explicitly cast any + objects implementing `UriInterface` to `string`. + +#### request() + +The `request(string $method, string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +send an arbitrary HTTP request. + +The preferred way to send an HTTP request is by using the above +[request methods](#request-methods), for example the [`get()`](#get) +method to send an HTTP `GET` request. + +As an alternative, if you want to use a custom HTTP request method, you +can use this method: + +```php +$browser->request('OPTIONS', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump((string)$response->getBody()); +}); +``` + +This method will automatically add a matching `Content-Length` request +header if the size of the outgoing request body is known and non-empty. +For an empty request body, if will only include a `Content-Length: 0` +request header if the request method usually expects a request body (only +applies to `POST`, `PUT` and `PATCH`). + +If you're using a streaming request body (`ReadableStreamInterface`), it +will default to using `Transfer-Encoding: chunked` or you have to +explicitly pass in a matching `Content-Length` request header like so: + +```php +$body = new React\Stream\ThroughStream(); +$loop->addTimer(1.0, function () use ($body) { + $body->end("hello world"); +}); + +$browser->request('POST', $url, array('Content-Length' => '11'), $body); +``` + +> Note that this method is available as of v2.9.0 and always buffers the + response body before resolving. + It does not respect the deprecated [`streaming` option](#withoptions). + If you want to stream the response body, you can use the + [`requestStreaming()`](#requeststreaming) method instead. + +#### requestStreaming() + +The `requestStreaming(string $method, string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +send an arbitrary HTTP request and receive a streaming response without buffering the response body. + +The preferred way to send an HTTP request is by using the above +[request methods](#request-methods), for example the [`get()`](#get) +method to send an HTTP `GET` request. Each of these methods will buffer +the whole response body in memory by default. This is easy to get started +and works reasonably well for smaller responses. + +In some situations, it's a better idea to use a streaming approach, where +only small chunks have to be kept in memory. You can use this method to +send an arbitrary HTTP request and receive a streaming response. It uses +the same HTTP message API, but does not buffer the response body in +memory. It only processes the response body in small chunks as data is +received and forwards this data through [ReactPHP's Stream API](https://github.com/reactphp/stream). +This works for (any number of) responses of arbitrary sizes. + +```php +$browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + $body = $response->getBody(); + assert($body instanceof Psr\Http\Message\StreamInterface); + assert($body instanceof React\Stream\ReadableStreamInterface); + + $body->on('data', function ($chunk) { + echo $chunk; + }); + + $body->on('error', function (Exception $error) { + echo 'Error: ' . $error->getMessage() . PHP_EOL; + }); + + $body->on('close', function () { + echo '[DONE]' . PHP_EOL; + }); +}); +``` + +See also [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +and the [streaming response](#streaming-response) for more details, +examples and possible use-cases. + +This method will automatically add a matching `Content-Length` request +header if the size of the outgoing request body is known and non-empty. +For an empty request body, if will only include a `Content-Length: 0` +request header if the request method usually expects a request body (only +applies to `POST`, `PUT` and `PATCH`). + +If you're using a streaming request body (`ReadableStreamInterface`), it +will default to using `Transfer-Encoding: chunked` or you have to +explicitly pass in a matching `Content-Length` request header like so: + +```php +$body = new React\Stream\ThroughStream(); +$loop->addTimer(1.0, function () use ($body) { + $body->end("hello world"); +}); + +$browser->requestStreaming('POST', $url, array('Content-Length' => '11'), $body); +``` + +> Note that this method is available as of v2.9.0 and always resolves the + response without buffering the response body. + It does not respect the deprecated [`streaming` option](#withoptions). + If you want to buffer the response body, use can use the + [`request()`](#request) method instead. + +#### ~~submit()~~ + +> Deprecated since v2.9.0, see [`post()`](#post) instead. + +The deprecated `submit(string|UriInterface $url, array $fields, array $headers = array(), string $method = 'POST'): PromiseInterface` method can be used to +submit an array of field values similar to submitting a form (`application/x-www-form-urlencoded`). + +```php +// deprecated: see post() instead +$browser->submit($url, array('user' => 'test', 'password' => 'secret')); +``` + +> For BC reasons, this method accepts the `$url` as either a `string` + value or as an `UriInterface`. It's recommended to explicitly cast any + objects implementing `UriInterface` to `string`. + +#### ~~send()~~ + +> Deprecated since v2.9.0, see [`request()`](#request) instead. + +The deprecated `send(RequestInterface $request): PromiseInterface` method can be used to +send an arbitrary instance implementing the [`RequestInterface`](#requestinterface) (PSR-7). + +The preferred way to send an HTTP request is by using the above +[request methods](#request-methods), for example the [`get()`](#get) +method to send an HTTP `GET` request. + +As an alternative, if you want to use a custom HTTP request method, you +can use this method: + +```php +$request = new Request('OPTIONS', $url); + +// deprecated: see request() instead +$browser->send($request)->then(…); +``` + +This method will automatically add a matching `Content-Length` request +header if the size of the outgoing request body is known and non-empty. +For an empty request body, if will only include a `Content-Length: 0` +request header if the request method usually expects a request body (only +applies to `POST`, `PUT` and `PATCH`). + +#### withTimeout() + +The `withTimeout(bool|number $timeout): Browser` method can be used to +change the maximum timeout used for waiting for pending requests. + +You can pass in the number of seconds to use as a new timeout value: + +```php +$browser = $browser->withTimeout(10.0); +``` + +You can pass in a bool `false` to disable any timeouts. In this case, +requests can stay pending forever: + +```php +$browser = $browser->withTimeout(false); +``` + +You can pass in a bool `true` to re-enable default timeout handling. This +will respects PHP's `default_socket_timeout` setting (default 60s): + +```php +$browser = $browser->withTimeout(true); +``` + +See also [timeouts](#timeouts) for more details about timeout handling. + +Notice that the [`Browser`](#browser) is an immutable object, i.e. this +method actually returns a *new* [`Browser`](#browser) instance with the +given timeout value applied. + +#### withFollowRedirects() + +The `withTimeout(bool|int $$followRedirects): Browser` method can be used to +change how HTTP redirects will be followed. + +You can pass in the maximum number of redirects to follow: + +```php +$new = $browser->withFollowRedirects(5); +``` + +The request will automatically be rejected when the number of redirects +is exceeded. You can pass in a `0` to reject the request for any +redirects encountered: + +```php +$browser = $browser->withFollowRedirects(0); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // only non-redirected responses will now end up here + var_dump($response->getHeaders()); +}); +``` + +You can pass in a bool `false` to disable following any redirects. In +this case, requests will resolve with the redirection response instead +of following the `Location` response header: + +```php +$browser = $browser->withFollowRedirects(false); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // any redirects will now end up here + var_dump($response->getHeaderLine('Location')); +}); +``` + +You can pass in a bool `true` to re-enable default redirect handling. +This defaults to following a maximum of 10 redirects: + +```php +$browser = $browser->withFollowRedirects(true); +``` + +See also [redirects](#redirects) for more details about redirect handling. + +Notice that the [`Browser`](#browser) is an immutable object, i.e. this +method actually returns a *new* [`Browser`](#browser) instance with the +given redirect setting applied. + +#### withRejectErrorResponse() + +The `withRejectErrorResponse(bool $obeySuccessCode): Browser` method can be used to +change whether non-successful HTTP response status codes (4xx and 5xx) will be rejected. + +You can pass in a bool `false` to disable rejecting incoming responses +that use a 4xx or 5xx response status code. In this case, requests will +resolve with the response message indicating an error condition: + +```php +$browser = $browser->withRejectErrorResponse(false); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // any HTTP response will now end up here + var_dump($response->getStatusCode(), $response->getReasonPhrase()); +}); +``` + +You can pass in a bool `true` to re-enable default status code handling. +This defaults to rejecting any response status codes in the 4xx or 5xx +range with a [`ResponseException`](#responseexception): + +```php +$browser = $browser->withRejectErrorResponse(true); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // any successful HTTP response will now end up here + var_dump($response->getStatusCode(), $response->getReasonPhrase()); +}, function (Exception $e) { + if ($e instanceof React\Http\Message\ResponseException) { + // any HTTP response error message will now end up here + $response = $e->getResponse(); + var_dump($response->getStatusCode(), $response->getReasonPhrase()); + } else { + var_dump($e->getMessage()); + } +}); +``` + +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. + +#### withBase() + +The `withBase(string|null|UriInterface $baseUrl): Browser` method can be used to +change the base URL used to resolve relative URLs to. + +If you configure a base URL, any requests to relative URLs will be +processed by first prepending this absolute base URL. Note that this +merely prepends the base URL and does *not* resolve any relative path +references (like `../` etc.). This is mostly useful for (RESTful) API +calls where all endpoints (URLs) are located under a common base URL. + +```php +$browser = $browser->withBase('http://api.example.com/v3'); + +// will request http://api.example.com/v3/example +$browser->get('/example')->then(…); +``` + +You can pass in a `null` base URL to return a new instance that does not +use a base URL: + +```php +$browser = $browser->withBase(null); +``` + +Accordingly, any requests using relative URLs to a browser that does not +use a base URL can not be completed and will be rejected without sending +a request. + +This method will throw an `InvalidArgumentException` if the given +`$baseUrl` argument is not a valid URL. + +Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withBase()` method +actually returns a *new* [`Browser`](#browser) instance with the given base URL applied. + +> For BC reasons, this method accepts the `$baseUrl` as either a `string` + value or as an `UriInterface`. It's recommended to explicitly cast any + objects implementing `UriInterface` to `string`. + +> Changelog: As of v2.9.0 this method accepts a `null` value to reset the + base URL. Earlier versions had to use the deprecated `withoutBase()` + method to reset the base URL. + +#### withProtocolVersion() + +The `withProtocolVersion(string $protocolVersion): Browser` method can be used to +change the HTTP protocol version that will be used for all subsequent requests. + +All the above [request methods](#request-methods) default to sending +requests as HTTP/1.1. This is the preferred HTTP protocol version which +also provides decent backwards-compatibility with legacy HTTP/1.0 +servers. As such, there should rarely be a need to explicitly change this +protocol version. + +If you want to explicitly use the legacy HTTP/1.0 protocol version, you +can use this method: + +```php +$newBrowser = $browser->withProtocolVersion('1.0'); + +$newBrowser->get($url)->then(…); +``` + +Notice that the [`Browser`](#browser) is an immutable object, i.e. this +method actually returns a *new* [`Browser`](#browser) instance with the +new protocol version applied. + +#### withResponseBuffer() + +The `withRespomseBuffer(int $maximumSize): Browser` method can be used to +change the maximum size for buffering a response body. + +The preferred way to send an HTTP request is by using the above +[request methods](#request-methods), for example the [`get()`](#get) +method to send an HTTP `GET` request. Each of these methods will buffer +the whole response body in memory by default. This is easy to get started +and works reasonably well for smaller responses. + +By default, the response body buffer will be limited to 16 MiB. If the +response body exceeds this maximum size, the request will be rejected. + +You can pass in the maximum number of bytes to buffer: + +```php +$browser = $browser->withResponseBuffer(1024 * 1024); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // response body will not exceed 1 MiB + var_dump($response->getHeaders(), (string) $response->getBody()); +}); +``` + +Note that the response body buffer has to be kept in memory for each +pending request until its transfer is completed and it will only be freed +after a pending request is fulfilled. As such, increasing this maximum +buffer size to allow larger response bodies is usually not recommended. +Instead, you can use the [`requestStreaming()` method](#requeststreaming) +to receive responses with arbitrary sizes without buffering. Accordingly, +this maximum buffer size setting has no effect on streaming responses. + +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. + +#### ~~withOptions()~~ + +> Deprecated since v2.9.0, see [`withTimeout()`](#withtimeout), [`withFollowRedirects()`](#withfollowredirects) + and [`withRejectErrorResponse()`](#withrejecterrorresponse) instead. + +The deprecated `withOptions(array $options): Browser` method can be used to +change the options to use: + +The [`Browser`](#browser) class exposes several options for the handling of +HTTP transactions. These options resemble some of PHP's +[HTTP context options](https://www.php.net/manual/en/context.http.php) and +can be controlled via the following API (and their defaults): + +```php +// deprecated +$newBrowser = $browser->withOptions(array( + 'timeout' => null, // see withTimeout() instead + 'followRedirects' => true, // see withFollowRedirects() instead + 'maxRedirects' => 10, // see withFollowRedirects() instead + 'obeySuccessCode' => true, // see withRejectErrorResponse() instead + 'streaming' => false, // deprecated, see requestStreaming() instead +)); +``` + +See also [timeouts](#timeouts), [redirects](#redirects) and +[streaming](#streaming-response) for more details. + +Notice that the [`Browser`](#browser) is an immutable object, i.e. this +method actually returns a *new* [`Browser`](#browser) instance with the +options applied. + +#### ~~withoutBase()~~ + +> Deprecated since v2.9.0, see [`withBase()`](#withbase) instead. + +The deprecated `withoutBase(): Browser` method can be used to +remove the base URL. + +```php +// deprecated: see withBase() instead +$newBrowser = $browser->withoutBase(); +``` + +Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withoutBase()` method +actually returns a *new* [`Browser`](#browser) instance without any base URL applied. + +See also [`withBase()`](#withbase). + ### React\Http\Middleware #### StreamingRequestMiddleware @@ -1470,6 +2749,51 @@ new RequestBodyParserMiddleware(10 * 1024, 100); // 100 files with 10 KiB each If you want to respect this setting, you have to check its value and effectively avoid using this middleware entirely. +### ResponseInterface + +The `Psr\Http\Message\ResponseInterface` represents the incoming response received from the [`Browser`](#browser). + +This is a standard interface defined in +[PSR-7: HTTP message interfaces](https://www.php-fig.org/psr/psr-7/), see its +[`ResponseInterface` definition](https://www.php-fig.org/psr/psr-7/#3-3-psr-http-message-responseinterface) +which in turn extends the +[`MessageInterface` definition](https://www.php-fig.org/psr/psr-7/#3-1-psr-http-message-messageinterface). + +### RequestInterface + +The `Psr\Http\Message\RequestInterface` represents the outgoing request to be sent via the [`Browser`](#browser). + +This is a standard interface defined in +[PSR-7: HTTP message interfaces](https://www.php-fig.org/psr/psr-7/), see its +[`RequestInterface` definition](https://www.php-fig.org/psr/psr-7/#3-2-psr-http-message-requestinterface) +which in turn extends the +[`MessageInterface` definition](https://www.php-fig.org/psr/psr-7/#3-1-psr-http-message-messageinterface). + +### UriInterface + +The `Psr\Http\Message\UriInterface` represents an absolute or relative URI (aka URL). + +This is a standard interface defined in +[PSR-7: HTTP message interfaces](https://www.php-fig.org/psr/psr-7/), see its +[`UriInterface` definition](https://www.php-fig.org/psr/psr-7/#3-5-psr-http-message-uriinterface). + +> For BC reasons, the request methods accept the URL as either a `string` + value or as an `UriInterface`. It's recommended to explicitly cast any + objects implementing `UriInterface` to `string`. + +### ResponseException + +The `ResponseException` is an `Exception` sub-class that will be used to reject +a request promise if the remote server returns a non-success status code +(anything but 2xx or 3xx). +You can control this behavior via the [`withRejectErrorResponse()` method](#withrejecterrorresponse). + +The `getCode(): int` method can be used to +return the HTTP response status code. + +The `getResponse(): ResponseInterface` method can be used to +access its underlying [`ResponseInterface`](#responseinterface) object. + ## Install The recommended way to install this library is [through Composer](https://getcomposer.org). diff --git a/composer.json b/composer.json index d750445a..755e5d82 100644 --- a/composer.json +++ b/composer.json @@ -5,15 +5,21 @@ "license": "MIT", "require": { "php": ">=5.3.0", - "ringcentral/psr7": "^1.2", - "react/socket": "^1.0 || ^0.8.3", - "react/stream": "^1.0 || ^0.7.1", - "react/promise": "^2.3 || ^1.2.1", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "react/promise-stream": "^1.1" + "psr/http-message": "^1.0", + "react/event-loop": "^1.0 || ^0.5", + "react/http-client": "^0.5.10", + "react/promise": "^2.3 || ^1.2.1", + "react/promise-stream": "^1.1", + "react/socket": "^1.1", + "react/stream": "^1.0 || ^0.7.5", + "ringcentral/psr7": "^1.2" }, "require-dev": { "clue/block-react": "^1.1", + "clue/http-proxy-react": "^1.3", + "clue/reactphp-ssh-proxy": "^1.0", + "clue/socks-react": "^1.0", "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35" }, "autoload": { diff --git a/examples/01-google.php b/examples/01-google.php new file mode 100644 index 00000000..31a82606 --- /dev/null +++ b/examples/01-google.php @@ -0,0 +1,15 @@ +get('http://google.com/')->then(function (ResponseInterface $response) { + var_dump($response->getHeaders(), (string)$response->getBody()); +}); + +$loop->run(); diff --git a/examples/02-concurrent.php b/examples/02-concurrent.php new file mode 100644 index 00000000..5a9e4258 --- /dev/null +++ b/examples/02-concurrent.php @@ -0,0 +1,23 @@ +head('http://www.github.com/clue/http-react')->then(function (ResponseInterface $response) { + var_dump($response->getHeaders(), (string)$response->getBody()); +}); + +$client->get('http://google.com/')->then(function (ResponseInterface $response) { + var_dump($response->getHeaders(), (string)$response->getBody()); +}); + +$client->get('http://www.lueck.tv/psocksd')->then(function (ResponseInterface $response) { + var_dump($response->getHeaders(), (string)$response->getBody()); +}); + +$loop->run(); diff --git a/examples/03-any.php b/examples/03-any.php new file mode 100644 index 00000000..881dabfc --- /dev/null +++ b/examples/03-any.php @@ -0,0 +1,32 @@ +head('http://www.github.com/clue/http-react'), + $client->get('https://httpbin.org/'), + $client->get('https://google.com/'), + $client->get('http://www.lueck.tv/psocksd'), + $client->get('http://www.httpbin.org/absolute-redirect/5') +); + +React\Promise\any($promises)->then(function (ResponseInterface $response) use ($promises) { + // first response arrived => cancel all other pending requests + foreach ($promises as $promise) { + $promise->cancel(); + } + + var_dump($response->getHeaders()); + echo PHP_EOL . $response->getBody(); +}); + +$loop->run(); diff --git a/examples/04-post-json.php b/examples/04-post-json.php new file mode 100644 index 00000000..818dc9bc --- /dev/null +++ b/examples/04-post-json.php @@ -0,0 +1,29 @@ + array( + 'first' => 'Alice', + 'name' => 'Smith' + ), + 'email' => 'alice@example.com' +); + +$client->post( + 'https://httpbin.org/post', + array( + 'Content-Type' => 'application/json' + ), + json_encode($data) +)->then(function (ResponseInterface $response) { + echo (string)$response->getBody(); +}, 'printf'); + +$loop->run(); diff --git a/examples/05-put-xml.php b/examples/05-put-xml.php new file mode 100644 index 00000000..7c23182d --- /dev/null +++ b/examples/05-put-xml.php @@ -0,0 +1,26 @@ +'); +$child = $xml->addChild('user'); +$child->alias = 'clue'; +$child->name = 'Christian Lück'; + +$client->put( + 'https://httpbin.org/put', + array( + 'Content-Type' => 'text/xml' + ), + $xml->asXML() +)->then(function (ResponseInterface $response) { + echo (string)$response->getBody(); +}, 'printf'); + +$loop->run(); diff --git a/examples/11-http-proxy.php b/examples/11-http-proxy.php new file mode 100644 index 00000000..d1ad9cf5 --- /dev/null +++ b/examples/11-http-proxy.php @@ -0,0 +1,29 @@ + $proxy, + 'dns' => false +)); +$browser = new Browser($loop, $connector); + +// demo fetching HTTP headers (or bail out otherwise) +$browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { + echo RingCentral\Psr7\str($response); +}, 'printf'); + +$loop->run(); diff --git a/examples/12-socks-proxy.php b/examples/12-socks-proxy.php new file mode 100644 index 00000000..3b694804 --- /dev/null +++ b/examples/12-socks-proxy.php @@ -0,0 +1,29 @@ + $proxy, + 'dns' => false +)); +$browser = new Browser($loop, $connector); + +// demo fetching HTTP headers (or bail out otherwise) +$browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { + echo RingCentral\Psr7\str($response); +}, 'printf'); + +$loop->run(); diff --git a/examples/13-ssh-proxy.php b/examples/13-ssh-proxy.php new file mode 100644 index 00000000..d0424fea --- /dev/null +++ b/examples/13-ssh-proxy.php @@ -0,0 +1,29 @@ + $proxy, + 'dns' => false +)); +$browser = new Browser($loop, $connector); + +// demo fetching HTTP headers (or bail out otherwise) +$browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { + echo RingCentral\Psr7\str($response); +}, 'printf'); + +$loop->run(); diff --git a/examples/14-unix-domain-sockets.php b/examples/14-unix-domain-sockets.php new file mode 100644 index 00000000..8881321e --- /dev/null +++ b/examples/14-unix-domain-sockets.php @@ -0,0 +1,27 @@ +get('http://localhost/info')->then(function (ResponseInterface $response) { + echo Psr7\str($response); +}, 'printf'); + +$loop->run(); diff --git a/examples/21-stream-forwarding.php b/examples/21-stream-forwarding.php new file mode 100644 index 00000000..b7873775 --- /dev/null +++ b/examples/21-stream-forwarding.php @@ -0,0 +1,33 @@ +write('Requesting ' . $url . '…' . PHP_EOL); + +$client->requestStreaming('GET', $url)->then(function (ResponseInterface $response) use ($info, $out) { + $info->write('Received' . PHP_EOL . Psr7\str($response)); + + $body = $response->getBody(); + assert($body instanceof ReadableStreamInterface); + $body->pipe($out); +}, 'printf'); + +$loop->run(); diff --git a/examples/22-stream-stdin.php b/examples/22-stream-stdin.php new file mode 100644 index 00000000..4a36df91 --- /dev/null +++ b/examples/22-stream-stdin.php @@ -0,0 +1,27 @@ +post($url, array(), $in)->then(function (ResponseInterface $response) { + echo 'Received' . PHP_EOL . Psr7\str($response); +}, 'printf'); + +$loop->run(); diff --git a/examples/91-benchmark-download.php b/examples/91-benchmark-download.php new file mode 100644 index 00000000..10ad8e00 --- /dev/null +++ b/examples/91-benchmark-download.php @@ -0,0 +1,61 @@ +requestStreaming('GET', $url)->then(function (ResponseInterface $response) use ($loop) { + echo 'Headers received' . PHP_EOL; + echo RingCentral\Psr7\str($response); + + $stream = $response->getBody(); + assert($stream instanceof ReadableStreamInterface); + + // count number of bytes received + $bytes = 0; + $stream->on('data', function ($chunk) use (&$bytes) { + $bytes += strlen($chunk); + }); + + // report progress every 0.1s + $timer = $loop->addPeriodicTimer(0.1, function () use (&$bytes) { + echo "\rDownloaded " . $bytes . " bytes…"; + }); + + // report results once the stream closes + $time = microtime(true); + $stream->on('close', function() use (&$bytes, $timer, $loop, $time) { + $loop->cancelTimer($timer); + + $time = microtime(true) - $time; + + echo "\r" . 'Downloaded ' . $bytes . ' bytes in ' . round($time, 3) . 's => ' . round($bytes / $time / 1000000, 1) . ' MB/s' . PHP_EOL; + }); +}, 'printf'); + +$loop->run(); diff --git a/examples/92-benchmark-upload.php b/examples/92-benchmark-upload.php new file mode 100644 index 00000000..2b4e7ed6 --- /dev/null +++ b/examples/92-benchmark-upload.php @@ -0,0 +1,125 @@ +chunk = $chunk; + $this->count = $count; + } + + public function pause() + { + $this->paused = true; + } + + public function resume() + { + if (!$this->paused || $this->closed) { + return; + } + + // keep emitting until stream is paused + $this->paused = false; + while ($this->position < $this->count && !$this->paused) { + ++$this->position; + $this->emit('data', array($this->chunk)); + } + + // end once the last chunk has been written + if ($this->position >= $this->count) { + $this->emit('end'); + $this->close(); + } + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + return Util::pipe($this, $dest, $options); + } + + public function isReadable() + { + return !$this->closed; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + $this->count = 0; + $this->paused = true; + $this->emit('close'); + } + + public function getPosition() + { + return $this->position * strlen($this->chunk); + } +} + +$loop = React\EventLoop\Factory::create(); +$client = new Browser($loop); + +$url = isset($argv[1]) ? $argv[1] : 'http://httpbin.org/post'; +$n = isset($argv[2]) ? $argv[2] : 10; +$source = new ChunkRepeater(str_repeat('x', 1000000), $n); +$loop->futureTick(function () use ($source) { + $source->resume(); +}); + +echo 'POSTing ' . $n . ' MB to ' . $url . PHP_EOL; + +$start = microtime(true); +$report = $loop->addPeriodicTimer(0.05, function () use ($source, $start) { + printf("\r%d bytes in %0.3fs...", $source->getPosition(), microtime(true) - $start); +}); + +$client->post($url, array('Content-Length' => $n * 1000000), $source)->then(function (ResponseInterface $response) use ($source, $report, $loop, $start) { + $now = microtime(true); + $loop->cancelTimer($report); + + printf("\r%d bytes in %0.3fs => %.1f MB/s\n", $source->getPosition(), $now - $start, $source->getPosition() / ($now - $start) / 1000000); + + echo rtrim(preg_replace('/x{5,}/','x…', (string) $response->getBody()), PHP_EOL) . PHP_EOL; +}, function ($e) use ($loop, $report) { + $loop->cancelTimer($report); + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); + +$loop->run(); diff --git a/src/Browser.php b/src/Browser.php new file mode 100644 index 00000000..70e875a2 --- /dev/null +++ b/src/Browser.php @@ -0,0 +1,867 @@ + '127.0.0.1', + * 'tcp' => array( + * 'bindto' => '192.168.10.1:0' + * ), + * 'tls' => array( + * 'verify_peer' => false, + * 'verify_peer_name' => false + * ) + * )); + * + * $browser = new React\Http\Browser($loop, $connector); + * ``` + * + * @param LoopInterface $loop + * @param ConnectorInterface|null $connector [optional] Connector to use. + * Should be `null` in order to use default Connector. + */ + public function __construct(LoopInterface $loop, ConnectorInterface $connector = null) + { + $this->messageFactory = new MessageFactory(); + $this->transaction = new Transaction( + Sender::createFromLoop($loop, $connector, $this->messageFactory), + $this->messageFactory, + $loop + ); + } + + /** + * Sends an HTTP GET request + * + * ```php + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }); + * ``` + * + * See also [example 01](../examples/01-google.php). + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $headers + * @return PromiseInterface + */ + public function get($url, array $headers = array()) + { + return $this->requestMayBeStreaming('GET', $url, $headers); + } + + /** + * Sends an HTTP POST request + * + * ```php + * $browser->post( + * $url, + * [ + * 'Content-Type' => 'application/json' + * ], + * json_encode($data) + * )->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump(json_decode((string)$response->getBody())); + * }); + * ``` + * + * See also [example 04](../examples/04-post-json.php). + * + * This method is also commonly used to submit HTML form data: + * + * ```php + * $data = [ + * 'user' => 'Alice', + * 'password' => 'secret' + * ]; + * + * $browser->post( + * $url, + * [ + * 'Content-Type' => 'application/x-www-form-urlencoded' + * ], + * http_build_query($data) + * ); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header if the outgoing request body is a `string`. If you're using a + * streaming request body (`ReadableStreamInterface`), it will default to + * using `Transfer-Encoding: chunked` or you have to explicitly pass in a + * matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * $loop->addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->post($url, array('Content-Length' => '11'), $body); + * ``` + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $contents + * @return PromiseInterface + */ + public function post($url, array $headers = array(), $contents = '') + { + return $this->requestMayBeStreaming('POST', $url, $headers, $contents); + } + + /** + * Sends an HTTP HEAD request + * + * ```php + * $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump($response->getHeaders()); + * }); + * ``` + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $headers + * @return PromiseInterface + */ + public function head($url, array $headers = array()) + { + return $this->requestMayBeStreaming('HEAD', $url, $headers); + } + + /** + * Sends an HTTP PATCH request + * + * ```php + * $browser->patch( + * $url, + * [ + * 'Content-Type' => 'application/json' + * ], + * json_encode($data) + * )->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump(json_decode((string)$response->getBody())); + * }); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header if the outgoing request body is a `string`. If you're using a + * streaming request body (`ReadableStreamInterface`), it will default to + * using `Transfer-Encoding: chunked` or you have to explicitly pass in a + * matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * $loop->addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->patch($url, array('Content-Length' => '11'), $body); + * ``` + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $contents + * @return PromiseInterface + */ + public function patch($url, array $headers = array(), $contents = '') + { + return $this->requestMayBeStreaming('PATCH', $url , $headers, $contents); + } + + /** + * Sends an HTTP PUT request + * + * ```php + * $browser->put( + * $url, + * [ + * 'Content-Type' => 'text/xml' + * ], + * $xml->asXML() + * )->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }); + * ``` + * + * See also [example 05](../examples/05-put-xml.php). + * + * This method will automatically add a matching `Content-Length` request + * header if the outgoing request body is a `string`. If you're using a + * streaming request body (`ReadableStreamInterface`), it will default to + * using `Transfer-Encoding: chunked` or you have to explicitly pass in a + * matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * $loop->addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->put($url, array('Content-Length' => '11'), $body); + * ``` + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $contents + * @return PromiseInterface + */ + public function put($url, array $headers = array(), $contents = '') + { + return $this->requestMayBeStreaming('PUT', $url, $headers, $contents); + } + + /** + * Sends an HTTP DELETE request + * + * ```php + * $browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }); + * ``` + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $contents + * @return PromiseInterface + */ + public function delete($url, array $headers = array(), $contents = '') + { + return $this->requestMayBeStreaming('DELETE', $url, $headers, $contents); + } + + /** + * Sends an arbitrary HTTP request. + * + * The preferred way to send an HTTP request is by using the above + * [request methods](#request-methods), for example the [`get()`](#get) + * method to send an HTTP `GET` request. + * + * As an alternative, if you want to use a custom HTTP request method, you + * can use this method: + * + * ```php + * $browser->request('OPTIONS', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header if the size of the outgoing request body is known and non-empty. + * For an empty request body, if will only include a `Content-Length: 0` + * request header if the request method usually expects a request body (only + * applies to `POST`, `PUT` and `PATCH`). + * + * If you're using a streaming request body (`ReadableStreamInterface`), it + * will default to using `Transfer-Encoding: chunked` or you have to + * explicitly pass in a matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * $loop->addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->request('POST', $url, array('Content-Length' => '11'), $body); + * ``` + * + * > Note that this method is available as of v2.9.0 and always buffers the + * response body before resolving. + * It does not respect the deprecated [`streaming` option](#withoptions). + * If you want to stream the response body, you can use the + * [`requestStreaming()`](#requeststreaming) method instead. + * + * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. + * @param string $url URL for the request + * @param array $headers Additional request headers + * @param string|ReadableStreamInterface $body HTTP request body contents + * @return PromiseInterface + * @since 2.9.0 + */ + public function request($method, $url, array $headers = array(), $body = '') + { + return $this->withOptions(array('streaming' => false))->requestMayBeStreaming($method, $url, $headers, $body); + } + + /** + * Sends an arbitrary HTTP request and receives a streaming response without buffering the response body. + * + * The preferred way to send an HTTP request is by using the above + * [request methods](#request-methods), for example the [`get()`](#get) + * method to send an HTTP `GET` request. Each of these methods will buffer + * the whole response body in memory by default. This is easy to get started + * and works reasonably well for smaller responses. + * + * In some situations, it's a better idea to use a streaming approach, where + * only small chunks have to be kept in memory. You can use this method to + * send an arbitrary HTTP request and receive a streaming response. It uses + * the same HTTP message API, but does not buffer the response body in + * memory. It only processes the response body in small chunks as data is + * received and forwards this data through [ReactPHP's Stream API](https://github.com/reactphp/stream). + * This works for (any number of) responses of arbitrary sizes. + * + * ```php + * $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * $body = $response->getBody(); + * assert($body instanceof Psr\Http\Message\StreamInterface); + * assert($body instanceof React\Stream\ReadableStreamInterface); + * + * $body->on('data', function ($chunk) { + * echo $chunk; + * }); + * + * $body->on('error', function (Exception $error) { + * echo 'Error: ' . $error->getMessage() . PHP_EOL; + * }); + * + * $body->on('close', function () { + * echo '[DONE]' . PHP_EOL; + * }); + * }); + * ``` + * + * See also [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) + * and the [streaming response](#streaming-response) for more details, + * examples and possible use-cases. + * + * This method will automatically add a matching `Content-Length` request + * header if the size of the outgoing request body is known and non-empty. + * For an empty request body, if will only include a `Content-Length: 0` + * request header if the request method usually expects a request body (only + * applies to `POST`, `PUT` and `PATCH`). + * + * If you're using a streaming request body (`ReadableStreamInterface`), it + * will default to using `Transfer-Encoding: chunked` or you have to + * explicitly pass in a matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * $loop->addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->requestStreaming('POST', $url, array('Content-Length' => '11'), $body); + * ``` + * + * > Note that this method is available as of v2.9.0 and always resolves the + * response without buffering the response body. + * It does not respect the deprecated [`streaming` option](#withoptions). + * If you want to buffer the response body, use can use the + * [`request()`](#request) method instead. + * + * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. + * @param string $url URL for the request + * @param array $headers Additional request headers + * @param string|ReadableStreamInterface $body HTTP request body contents + * @return PromiseInterface + * @since 2.9.0 + */ + public function requestStreaming($method, $url, $headers = array(), $contents = '') + { + return $this->withOptions(array('streaming' => true))->requestMayBeStreaming($method, $url, $headers, $contents); + } + + /** + * [Deprecated] Submits an array of field values similar to submitting a form (`application/x-www-form-urlencoded`). + * + * ```php + * // deprecated: see post() instead + * $browser->submit($url, array('user' => 'test', 'password' => 'secret')); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header for the encoded length of the given `$fields`. + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $fields + * @param array $headers + * @param string $method + * @return PromiseInterface + * @deprecated 2.9.0 See self::post() instead. + * @see self::post() + */ + public function submit($url, array $fields, $headers = array(), $method = 'POST') + { + $headers['Content-Type'] = 'application/x-www-form-urlencoded'; + $contents = http_build_query($fields); + + return $this->requestMayBeStreaming($method, $url, $headers, $contents); + } + + /** + * [Deprecated] Sends an arbitrary instance implementing the [`RequestInterface`](#requestinterface) (PSR-7). + * + * The preferred way to send an HTTP request is by using the above + * [request methods](#request-methods), for example the [`get()`](#get) + * method to send an HTTP `GET` request. + * + * As an alternative, if you want to use a custom HTTP request method, you + * can use this method: + * + * ```php + * $request = new Request('OPTIONS', $url); + * + * // deprecated: see request() instead + * $browser->send($request)->then(…); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header if the size of the outgoing request body is known and non-empty. + * For an empty request body, if will only include a `Content-Length: 0` + * request header if the request method usually expects a request body (only + * applies to `POST`, `PUT` and `PATCH`). + * + * @param RequestInterface $request + * @return PromiseInterface + * @deprecated 2.9.0 See self::request() instead. + * @see self::request() + */ + public function send(RequestInterface $request) + { + if ($this->baseUrl !== null) { + // ensure we're actually below the base URL + $request = $request->withUri($this->messageFactory->expandBase($request->getUri(), $this->baseUrl)); + } + + return $this->transaction->send($request); + } + + /** + * Changes the maximum timeout used for waiting for pending requests. + * + * You can pass in the number of seconds to use as a new timeout value: + * + * ```php + * $browser = $browser->withTimeout(10.0); + * ``` + * + * You can pass in a bool `false` to disable any timeouts. In this case, + * requests can stay pending forever: + * + * ```php + * $browser = $browser->withTimeout(false); + * ``` + * + * You can pass in a bool `true` to re-enable default timeout handling. This + * will respects PHP's `default_socket_timeout` setting (default 60s): + * + * ```php + * $browser = $browser->withTimeout(true); + * ``` + * + * See also [timeouts](#timeouts) for more details about timeout handling. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * given timeout value applied. + * + * @param bool|number $timeout + * @return self + */ + public function withTimeout($timeout) + { + if ($timeout === true) { + $timeout = null; + } elseif ($timeout === false) { + $timeout = -1; + } elseif ($timeout < 0) { + $timeout = 0; + } + + return $this->withOptions(array( + 'timeout' => $timeout, + )); + } + + /** + * Changes how HTTP redirects will be followed. + * + * You can pass in the maximum number of redirects to follow: + * + * ```php + * $new = $browser->withFollowRedirects(5); + * ``` + * + * The request will automatically be rejected when the number of redirects + * is exceeded. You can pass in a `0` to reject the request for any + * redirects encountered: + * + * ```php + * $browser = $browser->withFollowRedirects(0); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // only non-redirected responses will now end up here + * var_dump($response->getHeaders()); + * }); + * ``` + * + * You can pass in a bool `false` to disable following any redirects. In + * this case, requests will resolve with the redirection response instead + * of following the `Location` response header: + * + * ```php + * $browser = $browser->withFollowRedirects(false); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // any redirects will now end up here + * var_dump($response->getHeaderLine('Location')); + * }); + * ``` + * + * You can pass in a bool `true` to re-enable default redirect handling. + * This defaults to following a maximum of 10 redirects: + * + * ```php + * $browser = $browser->withFollowRedirects(true); + * ``` + * + * See also [redirects](#redirects) for more details about redirect handling. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * given redirect setting applied. + * + * @param bool|int $followRedirects + * @return self + */ + public function withFollowRedirects($followRedirects) + { + return $this->withOptions(array( + 'followRedirects' => $followRedirects !== false, + 'maxRedirects' => \is_bool($followRedirects) ? null : $followRedirects + )); + } + + /** + * Changes whether non-successful HTTP response status codes (4xx and 5xx) will be rejected. + * + * You can pass in a bool `false` to disable rejecting incoming responses + * that use a 4xx or 5xx response status code. In this case, requests will + * resolve with the response message indicating an error condition: + * + * ```php + * $browser = $browser->withRejectErrorResponse(false); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // any HTTP response will now end up here + * var_dump($response->getStatusCode(), $response->getReasonPhrase()); + * }); + * ``` + * + * You can pass in a bool `true` to re-enable default status code handling. + * This defaults to rejecting any response status codes in the 4xx or 5xx + * range: + * + * ```php + * $browser = $browser->withRejectErrorResponse(true); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // any successful HTTP response will now end up here + * var_dump($response->getStatusCode(), $response->getReasonPhrase()); + * }, function (Exception $e) { + * if ($e instanceof React\Http\Message\ResponseException) { + * // any HTTP response error message will now end up here + * $response = $e->getResponse(); + * var_dump($response->getStatusCode(), $response->getReasonPhrase()); + * } else { + * var_dump($e->getMessage()); + * } + * }); + * ``` + * + * 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. + * + * @param bool $obeySuccessCode + * @return self + */ + public function withRejectErrorResponse($obeySuccessCode) + { + return $this->withOptions(array( + 'obeySuccessCode' => $obeySuccessCode, + )); + } + + /** + * Changes the base URL used to resolve relative URLs to. + * + * If you configure a base URL, any requests to relative URLs will be + * processed by first prepending this absolute base URL. Note that this + * merely prepends the base URL and does *not* resolve any relative path + * references (like `../` etc.). This is mostly useful for (RESTful) API + * calls where all endpoints (URLs) are located under a common base URL. + * + * ```php + * $browser = $browser->withBase('http://api.example.com/v3'); + * + * // will request http://api.example.com/v3/example + * $browser->get('/example')->then(…); + * ``` + * + * You can pass in a `null` base URL to return a new instance that does not + * use a base URL: + * + * ```php + * $browser = $browser->withBase(null); + * ``` + * + * Accordingly, any requests using relative URLs to a browser that does not + * use a base URL can not be completed and will be rejected without sending + * a request. + * + * This method will throw an `InvalidArgumentException` if the given + * `$baseUrl` argument is not a valid URL. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withBase()` method + * actually returns a *new* [`Browser`](#browser) instance with the given base URL applied. + * + * > For BC reasons, this method accepts the `$baseUrl` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * > Changelog: As of v2.9.0 this method accepts a `null` value to reset the + * base URL. Earlier versions had to use the deprecated `withoutBase()` + * method to reset the base URL. + * + * @param string|null|UriInterface $baseUrl absolute base URL + * @return self + * @throws InvalidArgumentException if the given $baseUrl is not a valid absolute URL + * @see self::withoutBase() + */ + public function withBase($baseUrl) + { + $browser = clone $this; + if ($baseUrl === null) { + $browser->baseUrl = null; + return $browser; + } + + $browser->baseUrl = $this->messageFactory->uri($baseUrl); + if (!\in_array($browser->baseUrl->getScheme(), array('http', 'https')) || $browser->baseUrl->getHost() === '') { + throw new \InvalidArgumentException('Base URL must be absolute'); + } + + return $browser; + } + + /** + * Changes the HTTP protocol version that will be used for all subsequent requests. + * + * All the above [request methods](#request-methods) default to sending + * requests as HTTP/1.1. This is the preferred HTTP protocol version which + * also provides decent backwards-compatibility with legacy HTTP/1.0 + * servers. As such, there should rarely be a need to explicitly change this + * protocol version. + * + * If you want to explicitly use the legacy HTTP/1.0 protocol version, you + * can use this method: + * + * ```php + * $newBrowser = $browser->withProtocolVersion('1.0'); + * + * $newBrowser->get($url)->then(…); + * ``` + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * new protocol version applied. + * + * @param string $protocolVersion HTTP protocol version to use, must be one of "1.1" or "1.0" + * @return self + * @throws InvalidArgumentException + * @since 2.8.0 + */ + public function withProtocolVersion($protocolVersion) + { + if (!\in_array($protocolVersion, array('1.0', '1.1'), true)) { + throw new InvalidArgumentException('Invalid HTTP protocol version, must be one of "1.1" or "1.0"'); + } + + $browser = clone $this; + $browser->protocolVersion = (string) $protocolVersion; + + return $browser; + } + + /** + * Changes the maximum size for buffering a response body. + * + * The preferred way to send an HTTP request is by using the above + * [request methods](#request-methods), for example the [`get()`](#get) + * method to send an HTTP `GET` request. Each of these methods will buffer + * the whole response body in memory by default. This is easy to get started + * and works reasonably well for smaller responses. + * + * By default, the response body buffer will be limited to 16 MiB. If the + * response body exceeds this maximum size, the request will be rejected. + * + * You can pass in the maximum number of bytes to buffer: + * + * ```php + * $browser = $browser->withResponseBuffer(1024 * 1024); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // response body will not exceed 1 MiB + * var_dump($response->getHeaders(), (string) $response->getBody()); + * }); + * ``` + * + * Note that the response body buffer has to be kept in memory for each + * pending request until its transfer is completed and it will only be freed + * after a pending request is fulfilled. As such, increasing this maximum + * buffer size to allow larger response bodies is usually not recommended. + * Instead, you can use the [`requestStreaming()` method](#requeststreaming) + * to receive responses with arbitrary sizes without buffering. Accordingly, + * this maximum buffer size setting has no effect on streaming responses. + * + * 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. + * + * @param int $maximumSize + * @return self + * @see self::requestStreaming() + */ + public function withResponseBuffer($maximumSize) + { + return $this->withOptions(array( + 'maximumSize' => $maximumSize + )); + } + + /** + * [Deprecated] Changes the [options](#options) to use: + * + * The [`Browser`](#browser) class exposes several options for the handling of + * HTTP transactions. These options resemble some of PHP's + * [HTTP context options](http://php.net/manual/en/context.http.php) and + * can be controlled via the following API (and their defaults): + * + * ```php + * // deprecated + * $newBrowser = $browser->withOptions(array( + * 'timeout' => null, // see withTimeout() instead + * 'followRedirects' => true, // see withFollowRedirects() instead + * 'maxRedirects' => 10, // see withFollowRedirects() instead + * 'obeySuccessCode' => true, // see withRejectErrorResponse() instead + * 'streaming' => false, // deprecated, see requestStreaming() instead + * )); + * ``` + * + * See also [timeouts](#timeouts), [redirects](#redirects) and + * [streaming](#streaming) for more details. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * options applied. + * + * @param array $options + * @return self + * @deprecated 2.9.0 See self::withTimeout(), self::withFollowRedirects() and self::withRejectErrorResponse() instead. + * @see self::withTimeout() + * @see self::withFollowRedirects() + * @see self::withRejectErrorResponse() + */ + public function withOptions(array $options) + { + $browser = clone $this; + $browser->transaction = $this->transaction->withOptions($options); + + return $browser; + } + + /** + * [Deprecated] Removes the base URL. + * + * ```php + * // deprecated: see withBase() instead + * $newBrowser = $browser->withoutBase(); + * ``` + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withoutBase()` method + * actually returns a *new* [`Browser`](#browser) instance without any base URL applied. + * + * See also [`withBase()`](#withbase). + * + * @return self + * @deprecated 2.9.0 See self::withBase() instead. + * @see self::withBase() + */ + public function withoutBase() + { + return $this->withBase(null); + } + + /** + * @param string $method + * @param string|UriInterface $url + * @param array $headers + * @param string|ReadableStreamInterface $contents + * @return PromiseInterface + */ + private function requestMayBeStreaming($method, $url, array $headers = array(), $contents = '') + { + return $this->send($this->messageFactory->request($method, $url, $headers, $contents, $this->protocolVersion)); + } +} diff --git a/src/Io/ChunkedEncoder.php b/src/Io/ChunkedEncoder.php index d4e53b91..c84ef54f 100644 --- a/src/Io/ChunkedEncoder.php +++ b/src/Io/ChunkedEncoder.php @@ -17,7 +17,7 @@ class ChunkedEncoder extends EventEmitter implements ReadableStreamInterface { private $input; - private $closed; + private $closed = false; public function __construct(ReadableStreamInterface $input) { @@ -46,9 +46,7 @@ public function resume() public function pipe(WritableStreamInterface $dest, array $options = array()) { - Util::pipe($this, $dest, $options); - - return $dest; + return Util::pipe($this, $dest, $options); } public function close() @@ -67,13 +65,11 @@ public function close() /** @internal */ public function handleData($data) { - if ($data === '') { - return; + if ($data !== '') { + $this->emit('data', array( + \dechex(\strlen($data)) . "\r\n" . $data . "\r\n" + )); } - - $completeChunk = $this->createChunk($data); - - $this->emit('data', array($completeChunk)); } /** @internal */ @@ -93,18 +89,4 @@ public function handleEnd() $this->close(); } } - - /** - * @param string $data - string to be transformed in an valid - * HTTP encoded chunk string - * @return string - */ - private function createChunk($data) - { - $byteSize = \dechex(\strlen($data)); - $chunkBeginning = $byteSize . "\r\n"; - - return $chunkBeginning . $data . "\r\n"; - } - } diff --git a/src/Io/Sender.php b/src/Io/Sender.php new file mode 100644 index 00000000..e9c0a600 --- /dev/null +++ b/src/Io/Sender.php @@ -0,0 +1,161 @@ +http = $http; + $this->messageFactory = $messageFactory; + } + + /** + * + * @internal + * @param RequestInterface $request + * @return PromiseInterface Promise + */ + public function send(RequestInterface $request) + { + $body = $request->getBody(); + $size = $body->getSize(); + + if ($size !== null && $size !== 0) { + // automatically assign a "Content-Length" request header if the body size is known and non-empty + $request = $request->withHeader('Content-Length', (string)$size); + } 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')) { + // use "Transfer-Encoding: chunked" when this is a streaming body and body size is unknown + $request = $request->withHeader('Transfer-Encoding', 'chunked'); + } else { + // do not use chunked encoding if size is known or if this is an empty request body + $size = 0; + } + + $headers = array(); + foreach ($request->getHeaders() as $name => $values) { + $headers[$name] = implode(', ', $values); + } + + $requestStream = $this->http->request($request->getMethod(), (string)$request->getUri(), $headers, $request->getProtocolVersion()); + + $deferred = new Deferred(function ($_, $reject) use ($requestStream) { + // close request stream if request is cancelled + $reject(new \RuntimeException('Request cancelled')); + $requestStream->close(); + }); + + $requestStream->on('error', function($error) use ($deferred) { + $deferred->reject($error); + }); + + $messageFactory = $this->messageFactory; + $requestStream->on('response', function (ResponseStream $responseStream) use ($deferred, $messageFactory, $request) { + // apply response header values from response stream + $deferred->resolve($messageFactory->response( + $responseStream->getVersion(), + $responseStream->getCode(), + $responseStream->getReasonPhrase(), + $responseStream->getHeaders(), + $responseStream, + $request->getMethod() + )); + }); + + if ($body instanceof ReadableStreamInterface) { + if ($body->isReadable()) { + // length unknown => apply chunked transfer-encoding + if ($size === null) { + $body = new ChunkedEncoder($body); + } + + // pipe body into request stream + // add dummy write to immediately start request even if body does not emit any data yet + $body->pipe($requestStream); + $requestStream->write(''); + + $body->on('close', $close = function () use ($deferred, $requestStream) { + $deferred->reject(new \RuntimeException('Request failed because request body closed unexpectedly')); + $requestStream->close(); + }); + $body->on('error', function ($e) use ($deferred, $requestStream, $close, $body) { + $body->removeListener('close', $close); + $deferred->reject(new \RuntimeException('Request failed because request body reported an error', 0, $e)); + $requestStream->close(); + }); + $body->on('end', function () use ($close, $body) { + $body->removeListener('close', $close); + }); + } else { + // stream is not readable => end request without body + $requestStream->end(); + } + } else { + // body is fully buffered => write as one chunk + $requestStream->end((string)$body); + } + + return $deferred->promise(); + } +} diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php new file mode 100644 index 00000000..4b8cd390 --- /dev/null +++ b/src/Io/Transaction.php @@ -0,0 +1,305 @@ +sender = $sender; + $this->messageFactory = $messageFactory; + $this->loop = $loop; + } + + /** + * @param array $options + * @return self returns new instance, without modifying existing instance + */ + public function withOptions(array $options) + { + $transaction = clone $this; + foreach ($options as $name => $value) { + if (property_exists($transaction, $name)) { + // restore default value if null is given + if ($value === null) { + $default = new self($this->sender, $this->messageFactory, $this->loop); + $value = $default->$name; + } + + $transaction->$name = $value; + } + } + + return $transaction; + } + + public function send(RequestInterface $request) + { + $deferred = new Deferred(function () use (&$deferred) { + if (isset($deferred->pending)) { + $deferred->pending->cancel(); + unset($deferred->pending); + } + }); + + $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); + } + $timeout = -1; + $deferred->resolve($response); + }, + function ($e) use ($deferred, $loop, &$timeout) { + if (isset($deferred->timeout)) { + $loop->cancelTimer($deferred->timeout); + unset($deferred->timeout); + } + $timeout = -1; + $deferred->reject($e); + } + ); + + if ($timeout < 0) { + return $deferred->promise(); + } + + $body = $request->getBody(); + if ($body instanceof ReadableStreamInterface && $body->isReadable()) { + $that = $this; + $body->on('close', function () use ($that, $deferred, &$timeout) { + if ($timeout >= 0) { + $that->applyTimeout($deferred, $timeout); + } + }); + } else { + $this->applyTimeout($deferred, $timeout); + } + + return $deferred->promise(); + } + + /** + * @internal + * @param Deferred $deferred + * @param number $timeout + * @return void + */ + public function applyTimeout(Deferred $deferred, $timeout) + { + $deferred->timeout = $this->loop->addTimer($timeout, function () use ($timeout, $deferred) { + $deferred->reject(new \RuntimeException( + 'Request timed out after ' . $timeout . ' seconds' + )); + if (isset($deferred->pending)) { + $deferred->pending->cancel(); + unset($deferred->pending); + } + }); + } + + private function next(RequestInterface $request, Deferred $deferred) + { + $this->progress('request', array($request)); + + $that = $this; + ++$deferred->numRequests; + + $promise = $this->sender->send($request); + + if (!$this->streaming) { + $promise = $promise->then(function ($response) use ($deferred, $that) { + return $that->bufferResponse($response, $deferred); + }); + } + + $deferred->pending = $promise; + + return $promise->then( + function (ResponseInterface $response) use ($request, $that, $deferred) { + return $that->onResponse($response, $request, $deferred); + } + ); + } + + /** + * @internal + * @param ResponseInterface $response + * @return PromiseInterface Promise + */ + public function bufferResponse(ResponseInterface $response, $deferred) + { + $stream = $response->getBody(); + + $size = $stream->getSize(); + if ($size !== null && $size > $this->maximumSize) { + $stream->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 + )); + } + + // body is not streaming => already buffered + if (!$stream instanceof ReadableStreamInterface) { + return \React\Promise\resolve($response); + } + + // buffer stream and resolve with buffered body + $messageFactory = $this->messageFactory; + $maximumSize = $this->maximumSize; + $promise = \React\Promise\Stream\buffer($stream, $maximumSize)->then( + function ($body) use ($response, $messageFactory) { + return $response->withBody($messageFactory->body($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( + 'Response body size exceeds maximum of ' . $maximumSize . ' bytes', + \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 0 + ); + } + + throw $e; + } + ); + + $deferred->pending = $promise; + + return $promise; + } + + /** + * @internal + * @param ResponseInterface $response + * @param RequestInterface $request + * @throws ResponseException + * @return ResponseInterface|PromiseInterface + */ + public function onResponse(ResponseInterface $response, RequestInterface $request, $deferred) + { + $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); + } + + // only status codes 200-399 are considered to be valid, reject otherwise + if ($this->obeySuccessCode && ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400)) { + throw new ResponseException($response); + } + + // resolve our initial promise + return $response; + } + + /** + * @param ResponseInterface $response + * @param RequestInterface $request + * @return PromiseInterface + * @throws \RuntimeException + */ + private function onResponseRedirect(ResponseInterface $response, RequestInterface $request, Deferred $deferred) + { + // resolve location relative to last request URI + $location = $this->messageFactory->uriRelative($request->getUri(), $response->getHeaderLine('Location')); + + $request = $this->makeRedirectRequest($request, $location); + $this->progress('redirect', array($request)); + + if ($deferred->numRequests >= $this->maxRedirects) { + throw new \RuntimeException('Maximum number of redirects (' . $this->maxRedirects . ') exceeded'); + } + + return $this->next($request, $deferred); + } + + /** + * @param RequestInterface $request + * @param UriInterface $location + * @return RequestInterface + */ + private function makeRedirectRequest(RequestInterface $request, UriInterface $location) + { + $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). + if ($location->getHost() !== $originalHost) { + $request = $request->withoutHeader('Authorization'); + } + + // naïve approach.. + $method = ($request->getMethod() === 'HEAD') ? 'HEAD' : 'GET'; + + return $this->messageFactory->request($method, $location, $request->getHeaders()); + } + + private function progress($name, array $args = array()) + { + return; + + echo $name; + + foreach ($args as $arg) { + echo ' '; + if ($arg instanceof ResponseInterface) { + echo 'HTTP/' . $arg->getProtocolVersion() . ' ' . $arg->getStatusCode() . ' ' . $arg->getReasonPhrase(); + } elseif ($arg instanceof RequestInterface) { + echo $arg->getMethod() . ' ' . $arg->getRequestTarget() . ' HTTP/' . $arg->getProtocolVersion(); + } else { + echo $arg; + } + } + + echo PHP_EOL; + } +} diff --git a/src/Message/MessageFactory.php b/src/Message/MessageFactory.php new file mode 100644 index 00000000..eaa144cd --- /dev/null +++ b/src/Message/MessageFactory.php @@ -0,0 +1,139 @@ +body($content), $protocolVersion); + } + + /** + * Creates a new instance of ResponseInterface for the given response parameters + * + * @param string $protocolVersion + * @param int $status + * @param string $reason + * @param array $headers + * @param ReadableStreamInterface|string $body + * @param ?string $requestMethod + * @return Response + * @uses self::body() + */ + public function response($protocolVersion, $status, $reason, $headers = array(), $body = '', $requestMethod = null) + { + $response = new Response($status, $headers, $body instanceof ReadableStreamInterface ? null : $body, $protocolVersion, $reason); + + if ($body instanceof ReadableStreamInterface) { + $length = null; + $code = $response->getStatusCode(); + if ($requestMethod === 'HEAD' || ($code >= 100 && $code < 200) || $code == 204 || $code == 304) { + $length = 0; + } elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') { + $length = null; + } elseif ($response->hasHeader('Content-Length')) { + $length = (int)$response->getHeaderLine('Content-Length'); + } + + $response = $response->withBody(new ReadableBodyStream($body, $length)); + } + + return $response; + } + + /** + * Creates a new instance of StreamInterface for the given body contents + * + * @param ReadableStreamInterface|string $body + * @return StreamInterface + */ + public function body($body) + { + if ($body instanceof ReadableStreamInterface) { + return new ReadableBodyStream($body); + } + + return \RingCentral\Psr7\stream_for($body); + } + + /** + * Creates a new instance of UriInterface for the given URI string + * + * @param string $uri + * @return UriInterface + */ + public function uri($uri) + { + return new Uri($uri); + } + + /** + * Creates a new instance of UriInterface for the given URI string relative to the given base URI + * + * @param UriInterface $base + * @param string $uri + * @return UriInterface + */ + public function uriRelative(UriInterface $base, $uri) + { + return Uri::resolve($base, $uri); + } + + /** + * Resolves the given relative or absolute $uri by appending it behind $this base URI + * + * The given $uri parameter can be either a relative or absolute URI and + * as such can not contain any URI template placeholders. + * + * As such, the outcome of this method represents a valid, absolute URI + * which will be returned as an instance implementing `UriInterface`. + * + * If the given $uri is a relative URI, it will simply be appended behind $base URI. + * + * If the given $uri is an absolute URI, it will simply be returned as-is. + * + * @param UriInterface $uri + * @param UriInterface $base + * @return UriInterface + */ + public function expandBase(UriInterface $uri, UriInterface $base) + { + if ($uri->getScheme() !== '') { + return $uri; + } + + $uri = (string)$uri; + $base = (string)$base; + + if ($uri !== '' && substr($base, -1) !== '/' && substr($uri, 0, 1) !== '?') { + $base .= '/'; + } + + if (isset($uri[0]) && $uri[0] === '/') { + $uri = substr($uri, 1); + } + + return $this->uri($base . $uri); + } +} diff --git a/src/Message/ReadableBodyStream.php b/src/Message/ReadableBodyStream.php new file mode 100644 index 00000000..bb0064e0 --- /dev/null +++ b/src/Message/ReadableBodyStream.php @@ -0,0 +1,153 @@ +input = $input; + $this->size = $size; + + $that = $this; + $pos =& $this->position; + $input->on('data', function ($data) use ($that, &$pos, $size) { + $that->emit('data', array($data)); + + $pos += \strlen($data); + if ($size !== null && $pos >= $size) { + $that->handleEnd(); + } + }); + $input->on('error', function ($error) use ($that) { + $that->emit('error', array($error)); + $that->close(); + }); + $input->on('end', array($that, 'handleEnd')); + $input->on('close', array($that, 'close')); + } + + public function close() + { + if (!$this->closed) { + $this->closed = true; + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + } + + public function isReadable() + { + return $this->input->isReadable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function eof() + { + return !$this->isReadable(); + } + + public function __toString() + { + return ''; + } + + public function detach() + { + throw new \BadMethodCallException(); + } + + public function getSize() + { + return $this->size; + } + + public function tell() + { + throw new \BadMethodCallException(); + } + + public function isSeekable() + { + return false; + } + + public function seek($offset, $whence = SEEK_SET) + { + throw new \BadMethodCallException(); + } + + public function rewind() + { + throw new \BadMethodCallException(); + } + + public function isWritable() + { + return false; + } + + public function write($string) + { + throw new \BadMethodCallException(); + } + + public function read($length) + { + throw new \BadMethodCallException(); + } + + public function getContents() + { + throw new \BadMethodCallException(); + } + + public function getMetadata($key = null) + { + return ($key === null) ? array() : null; + } + + /** @internal */ + public function handleEnd() + { + if ($this->position !== $this->size && $this->size !== null) { + $this->emit('error', array(new \UnderflowException('Unexpected end of response body after ' . $this->position . '/' . $this->size . ' bytes'))); + } else { + $this->emit('end'); + } + + $this->close(); + } +} diff --git a/src/Message/ResponseException.php b/src/Message/ResponseException.php new file mode 100644 index 00000000..88272242 --- /dev/null +++ b/src/Message/ResponseException.php @@ -0,0 +1,43 @@ +getStatusCode() . ' (' . $response->getReasonPhrase() . ')'; + } + if ($code === null) { + $code = $response->getStatusCode(); + } + parent::__construct($message, $code, $previous); + + $this->response = $response; + } + + /** + * Access its underlying [`ResponseInterface`](#responseinterface) object. + * + * @return ResponseInterface + */ + public function getResponse() + { + return $this->response; + } +} diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php new file mode 100644 index 00000000..56a28303 --- /dev/null +++ b/tests/BrowserTest.php @@ -0,0 +1,414 @@ +loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $this->sender = $this->getMockBuilder('React\Http\Io\Transaction')->disableOriginalConstructor()->getMock(); + $this->browser = new Browser($this->loop); + + $ref = new \ReflectionProperty($this->browser, 'transaction'); + $ref->setAccessible(true); + $ref->setValue($this->browser, $this->sender); + } + + public function testGetSendsGetRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('GET', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('http://example.com/'); + } + + public function testPostSendsPostRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('POST', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->post('http://example.com/'); + } + + public function testHeadSendsHeadRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('HEAD', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->head('http://example.com/'); + } + + public function testPatchSendsPatchRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('PATCH', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->patch('http://example.com/'); + } + + public function testPutSendsPutRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('PUT', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->put('http://example.com/'); + } + + public function testDeleteSendsDeleteRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('DELETE', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->delete('http://example.com/'); + } + + public function testRequestOptionsSendsPutRequestWithStreamingExplicitlyDisabled() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('streaming' => false))->willReturnSelf(); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('OPTIONS', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->request('OPTIONS', 'http://example.com/'); + } + + public function testRequestStreamingGetSendsGetRequestWithStreamingExplicitlyEnabled() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('streaming' => true))->willReturnSelf(); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('GET', $request->getMethod()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->requestStreaming('GET', 'http://example.com/'); + } + + public function testSubmitSendsPostRequest() + { + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('POST', $request->getMethod()); + $that->assertEquals('application/x-www-form-urlencoded', $request->getHeaderLine('Content-Type')); + $that->assertEquals('', (string)$request->getBody()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->submit('http://example.com/', array()); + } + + public function testWithTimeoutTrueSetsDefaultTimeoutOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('timeout' => null))->willReturnSelf(); + + $this->browser->withTimeout(true); + } + + public function testWithTimeoutFalseSetsNegativeTimeoutOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('timeout' => -1))->willReturnSelf(); + + $this->browser->withTimeout(false); + } + + public function testWithTimeout10SetsTimeoutOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('timeout' => 10))->willReturnSelf(); + + $this->browser->withTimeout(10); + } + + public function testWithTimeoutNegativeSetsZeroTimeoutOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('timeout' => null))->willReturnSelf(); + + $this->browser->withTimeout(-10); + } + + public function testWithFollowRedirectsTrueSetsSenderOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('followRedirects' => true, 'maxRedirects' => null))->willReturnSelf(); + + $this->browser->withFollowRedirects(true); + } + + public function testWithFollowRedirectsFalseSetsSenderOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('followRedirects' => false, 'maxRedirects' => null))->willReturnSelf(); + + $this->browser->withFollowRedirects(false); + } + + public function testWithFollowRedirectsTenSetsSenderOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('followRedirects' => true, 'maxRedirects' => 10))->willReturnSelf(); + + $this->browser->withFollowRedirects(10); + } + + public function testWithFollowRedirectsZeroSetsSenderOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('followRedirects' => true, 'maxRedirects' => 0))->willReturnSelf(); + + $this->browser->withFollowRedirects(0); + } + + public function testWithRejectErrorResponseTrueSetsSenderOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('obeySuccessCode' => true))->willReturnSelf(); + + $this->browser->withRejectErrorResponse(true); + } + + public function testWithRejectErrorResponseFalseSetsSenderOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('obeySuccessCode' => false))->willReturnSelf(); + + $this->browser->withRejectErrorResponse(false); + } + + public function testWithResponseBufferThousandSetsSenderOption() + { + $this->sender->expects($this->once())->method('withOptions')->with(array('maximumSize' => 1000))->willReturnSelf(); + + $this->browser->withResponseBuffer(1000); + } + + public function testWithBase() + { + $browser = $this->browser->withBase('http://example.com/root'); + + $this->assertInstanceOf('React\Http\Browser', $browser); + $this->assertNotSame($this->browser, $browser); + } + + public function provideOtherUris() + { + return array( + 'empty returns base' => array( + 'http://example.com/base', + '', + 'http://example.com/base', + ), + 'absolute same as base returns base' => array( + 'http://example.com/base', + 'http://example.com/base', + 'http://example.com/base', + ), + 'absolute below base returns absolute' => array( + 'http://example.com/base', + 'http://example.com/base/another', + 'http://example.com/base/another', + ), + 'slash returns added slash' => array( + 'http://example.com/base', + '/', + 'http://example.com/base/', + ), + 'slash does not add duplicate slash if base already ends with slash' => array( + 'http://example.com/base/', + '/', + 'http://example.com/base/', + ), + 'relative is added behind base' => array( + 'http://example.com/base/', + 'test', + 'http://example.com/base/test', + ), + 'relative with slash is added behind base without duplicate slashes' => array( + 'http://example.com/base/', + '/test', + 'http://example.com/base/test', + ), + 'relative is added behind base with automatic slash inbetween' => array( + 'http://example.com/base', + 'test', + 'http://example.com/base/test', + ), + 'relative with slash is added behind base' => array( + 'http://example.com/base', + '/test', + 'http://example.com/base/test', + ), + 'query string is added behind base' => array( + 'http://example.com/base', + '?key=value', + 'http://example.com/base?key=value', + ), + 'query string is added behind base with slash' => array( + 'http://example.com/base/', + '?key=value', + 'http://example.com/base/?key=value', + ), + 'query string with slash is added behind base' => array( + 'http://example.com/base', + '/?key=value', + 'http://example.com/base/?key=value', + ), + 'absolute with query string below base is returned as-is' => array( + 'http://example.com/base', + 'http://example.com/base?test', + 'http://example.com/base?test', + ), + 'urlencoded special chars will stay as-is' => array( + 'http://example.com/%7Bversion%7D/', + '', + 'http://example.com/%7Bversion%7D/' + ), + 'special chars will be urlencoded' => array( + 'http://example.com/%7Bversion%7D/', + '', + 'http://example.com/%7Bversion%7D/' + ), + 'other domain' => array( + 'http://example.com/base/', + 'http://example.org/base/', + 'http://example.org/base/' + ), + 'other scheme' => array( + 'http://example.com/base/', + 'https://example.com/base/', + 'https://example.com/base/' + ), + 'other port' => array( + 'http://example.com/base/', + 'http://example.com:81/base/', + 'http://example.com:81/base/' + ), + 'other path' => array( + 'http://example.com/base/', + 'http://example.com/other/', + 'http://example.com/other/' + ), + 'other path due to missing slash' => array( + 'http://example.com/base/', + 'http://example.com/other', + 'http://example.com/other' + ), + ); + } + + /** + * @dataProvider provideOtherUris + * @param string $uri + * @param string $expected + */ + public function testResolveUriWithBaseEndsWithoutSlash($base, $uri, $expectedAbsolute) + { + $browser = $this->browser->withBase($base); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($expectedAbsolute, $that) { + $that->assertEquals($expectedAbsolute, $request->getUri()); + return true; + }))->willReturn(new Promise(function () { })); + + $browser->get($uri); + } + + public function testWithBaseUrlNotAbsoluteFails() + { + $this->setExpectedException('InvalidArgumentException'); + $this->browser->withBase('hello'); + } + + public function testWithBaseUrlInvalidSchemeFails() + { + $this->setExpectedException('InvalidArgumentException'); + $this->browser->withBase('ftp://example.com'); + } + + public function testWithoutBaseFollowedByGetRequestTriesToSendIncompleteRequestUrl() + { + $this->browser = $this->browser->withBase('http://example.com/')->withoutBase(); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('path', $request->getUri()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('path'); + } + + public function testWithProtocolVersionFollowedByGetRequestSendsRequestWithProtocolVersion() + { + $this->browser = $this->browser->withProtocolVersion('1.0'); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('1.0', $request->getProtocolVersion()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->get('http://example.com/'); + } + + public function testWithProtocolVersionFollowedBySubmitRequestSendsRequestWithProtocolVersion() + { + $this->browser = $this->browser->withProtocolVersion('1.0'); + + $that = $this; + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('1.0', $request->getProtocolVersion()); + return true; + }))->willReturn(new Promise(function () { })); + + $this->browser->submit('http://example.com/', array()); + } + + public function testWithProtocolVersionInvalidThrows() + { + $this->setExpectedException('InvalidArgumentException'); + $this->browser->withProtocolVersion('1.2'); + } + + public function testCancelGetRequestShouldCancelUnderlyingSocketConnection() + { + $pending = new Promise(function () { }, $this->expectCallableOnce()); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->with('example.com:80')->willReturn($pending); + + $this->browser = new Browser($this->loop, $connector); + + $promise = $this->browser->get('http://example.com/'); + $promise->cancel(); + } +} diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php new file mode 100644 index 00000000..16293fbb --- /dev/null +++ b/tests/FunctionalBrowserTest.php @@ -0,0 +1,645 @@ +loop = $loop = Factory::create(); + $this->browser = new Browser($this->loop); + + $server = new Server(array(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use ($loop) { + $path = $request->getUri()->getPath(); + + $headers = array(); + foreach ($request->getHeaders() as $name => $values) { + $headers[$name] = implode(', ', $values); + } + + if ($path === '/get') { + return new Response( + 200, + array(), + 'hello' + ); + } + + if ($path === '/redirect-to') { + $params = $request->getQueryParams(); + return new Response( + 302, + array('Location' => $params['url']) + ); + } + + if ($path === '/basic-auth/user/pass') { + return new Response( + $request->getHeaderLine('Authorization') === 'Basic dXNlcjpwYXNz' ? 200 : 401, + array(), + '' + ); + } + + if ($path === '/status/300') { + return new Response( + 300, + array(), + '' + ); + } + + if ($path === '/status/404') { + return new Response( + 404, + array(), + '' + ); + } + + if ($path === '/delay/10') { + return new Promise(function ($resolve) use ($loop) { + $loop->addTimer(10, function () use ($resolve) { + $resolve(new Response( + 200, + array(), + 'hello' + )); + }); + }); + } + + if ($path === '/post') { + return new Promise(function ($resolve) use ($request, $headers) { + $body = $request->getBody(); + assert($body instanceof ReadableStreamInterface); + + $buffer = ''; + $body->on('data', function ($data) use (&$buffer) { + $buffer .= $data; + }); + + $body->on('close', function () use (&$buffer, $resolve, $headers) { + $resolve(new Response( + 200, + array(), + json_encode(array( + 'data' => $buffer, + 'headers' => $headers + )) + )); + }); + }); + } + + if ($path === '/stream/1') { + $stream = new ThroughStream(); + + $loop->futureTick(function () use ($stream, $headers) { + $stream->end(json_encode(array( + 'headers' => $headers + ))); + }); + + return new Response( + 200, + array(), + $stream + ); + } + + var_dump($path); + })); + $socket = new \React\Socket\Server(0, $this->loop); + $server->listen($socket); + + $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; + } + + /** + * @doesNotPerformAssertions + */ + public function testSimpleRequest() + { + Block\await($this->browser->get($this->base . 'get'), $this->loop); + } + + public function testGetRequestWithRelativeAddressRejects() + { + $promise = $this->browser->get('delay'); + + $this->setExpectedException('InvalidArgumentException', 'Invalid request URL given'); + Block\await($promise, $this->loop); + } + + /** + * @doesNotPerformAssertions + */ + public function testGetRequestWithBaseAndRelativeAddressResolves() + { + Block\await($this->browser->withBase($this->base)->get('get'), $this->loop); + } + + /** + * @doesNotPerformAssertions + */ + public function testGetRequestWithBaseAndFullAddressResolves() + { + Block\await($this->browser->withBase('http://example.com/')->get($this->base . 'get'), $this->loop); + } + + public function testCancelGetRequestWillRejectRequest() + { + $promise = $this->browser->get($this->base . 'get'); + $promise->cancel(); + + $this->setExpectedException('RuntimeException'); + Block\await($promise, $this->loop); + } + + public function testCancelSendWithPromiseFollowerWillRejectRequest() + { + $promise = $this->browser->send(new Request('GET', $this->base . 'get'))->then(function () { + var_dump('noop'); + }); + $promise->cancel(); + + $this->setExpectedException('RuntimeException'); + Block\await($promise, $this->loop); + } + + public function testRequestWithoutAuthenticationFails() + { + $this->setExpectedException('RuntimeException'); + Block\await($this->browser->get($this->base . 'basic-auth/user/pass'), $this->loop); + } + + /** + * @doesNotPerformAssertions + */ + public function testRequestWithAuthenticationSucceeds() + { + $base = str_replace('://', '://user:pass@', $this->base); + + Block\await($this->browser->get($base . 'basic-auth/user/pass'), $this->loop); + } + + /** + * ```bash + * $ curl -vL "http://httpbin.org/redirect-to?url=http://user:pass@httpbin.org/basic-auth/user/pass" + * ``` + * + * @doesNotPerformAssertions + */ + public function testRedirectToPageWithAuthenticationSendsAuthenticationFromLocationHeader() + { + $target = str_replace('://', '://user:pass@', $this->base) . 'basic-auth/user/pass'; + + Block\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($target)), $this->loop); + } + + /** + * ```bash + * $ curl -vL "http://unknown:invalid@httpbin.org/redirect-to?url=http://user:pass@httpbin.org/basic-auth/user/pass" + * ``` + * + * @doesNotPerformAssertions + */ + public function testRedirectFromPageWithInvalidAuthToPageWithCorrectAuthenticationSucceeds() + { + $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)), $this->loop); + } + + public function testCancelRedirectedRequestShouldReject() + { + $promise = $this->browser->get($this->base . 'redirect-to?url=delay%2F10'); + + $this->loop->addTimer(0.1, function () use ($promise) { + $promise->cancel(); + }); + + $this->setExpectedException('RuntimeException', 'Request cancelled'); + Block\await($promise, $this->loop); + } + + 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, $this->loop); + } + + public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject() + { + $stream = new ThroughStream(); + $promise = $this->browser->withTimeout(0.1)->post($this->base . 'delay/10', array(), $stream); + $stream->end(); + + $this->setExpectedException('RuntimeException', 'Request timed out after 0.1 seconds'); + Block\await($promise, $this->loop); + } + + /** + * @doesNotPerformAssertions + */ + public function testTimeoutFalseShouldResolveSuccessfully() + { + Block\await($this->browser->withTimeout(false)->get($this->base . 'get'), $this->loop); + } + + /** + * @doesNotPerformAssertions + */ + public function testRedirectRequestRelative() + { + Block\await($this->browser->get($this->base . 'redirect-to?url=get'), $this->loop); + } + + /** + * @doesNotPerformAssertions + */ + public function testRedirectRequestAbsolute() + { + Block\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($this->base . 'get')), $this->loop); + } + + /** + * @doesNotPerformAssertions + */ + public function testFollowingRedirectsFalseResolvesWithRedirectResult() + { + $browser = $this->browser->withFollowRedirects(false); + + Block\await($browser->get($this->base . 'redirect-to?url=get'), $this->loop); + } + + public function testFollowRedirectsZeroRejectsOnRedirect() + { + $browser = $this->browser->withFollowRedirects(0); + + $this->setExpectedException('RuntimeException'); + Block\await($browser->get($this->base . 'redirect-to?url=get'), $this->loop); + } + + /** + * @doesNotPerformAssertions + */ + public function testResponseStatus300WithoutLocationShouldResolveWithoutFollowingRedirect() + { + Block\await($this->browser->get($this->base . 'status/300'), $this->loop); + } + + /** + * @doesNotPerformAssertions + */ + public function testGetRequestWithResponseBufferMatchedExactlyResolves() + { + $promise = $this->browser->withResponseBuffer(5)->get($this->base . 'get'); + + Block\await($promise, $this->loop); + } + + public function testGetRequestWithResponseBufferExceededRejects() + { + $promise = $this->browser->withResponseBuffer(4)->get($this->base . 'get'); + + $this->setExpectedException( + 'OverflowException', + 'Response body size of 5 bytes exceeds maximum of 4 bytes', + defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 + ); + Block\await($promise, $this->loop); + } + + public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() + { + $promise = $this->browser->withResponseBuffer(4)->get($this->base . 'stream/1'); + + $this->setExpectedException( + 'OverflowException', + 'Response body size exceeds maximum of 4 bytes', + defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 + ); + Block\await($promise, $this->loop); + } + + /** + * @group online + * @doesNotPerformAssertions + */ + public function testCanAccessHttps() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + Block\await($this->browser->get('https://www.google.com/'), $this->loop); + } + + /** + * @group online + */ + public function testVerifyPeerEnabledForBadSslRejects() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $connector = new Connector($this->loop, array( + 'tls' => array( + 'verify_peer' => true + ) + )); + + $browser = new Browser($this->loop, $connector); + + $this->setExpectedException('RuntimeException'); + Block\await($browser->get('https://self-signed.badssl.com/'), $this->loop); + } + + /** + * @group online + * @doesNotPerformAssertions + */ + public function testVerifyPeerDisabledForBadSslResolves() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $connector = new Connector($this->loop, array( + 'tls' => array( + 'verify_peer' => false + ) + )); + + $browser = new Browser($this->loop, $connector); + + Block\await($browser->get('https://self-signed.badssl.com/'), $this->loop); + } + + /** + * @group online + */ + public function testInvalidPort() + { + $this->setExpectedException('RuntimeException'); + Block\await($this->browser->get('http://www.google.com:443/'), $this->loop); + } + + public function testErrorStatusCodeRejectsWithResponseException() + { + try { + Block\await($this->browser->get($this->base . 'status/404'), $this->loop); + $this->fail(); + } catch (ResponseException $e) { + $this->assertEquals(404, $e->getCode()); + + $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $e->getResponse()); + $this->assertEquals(404, $e->getResponse()->getStatusCode()); + } + } + + public function testErrorStatusCodeDoesNotRejectWithRejectErrorResponseFalse() + { + $response = Block\await($this->browser->withRejectErrorResponse(false)->get($this->base . 'status/404'), $this->loop); + + $this->assertEquals(404, $response->getStatusCode()); + } + + public function testPostString() + { + $response = Block\await($this->browser->post($this->base . 'post', array(), 'hello world'), $this->loop); + $data = json_decode((string)$response->getBody(), true); + + $this->assertEquals('hello world', $data['data']); + } + + public function testReceiveStreamUntilConnectionsEndsForHttp10() + { + $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base . 'stream/1'), $this->loop); + + $this->assertEquals('1.0', $response->getProtocolVersion()); + $this->assertFalse($response->hasHeader('Transfer-Encoding')); + + $this->assertStringStartsWith('{', (string) $response->getBody()); + $this->assertStringEndsWith('}', (string) $response->getBody()); + } + + public function testReceiveStreamChunkedForHttp11() + { + $response = Block\await($this->browser->send(new Request('GET', $this->base . 'stream/1', array(), null, '1.1')), $this->loop); + + $this->assertEquals('1.1', $response->getProtocolVersion()); + + // underlying http-client automatically decodes and doesn't expose header + // @link https://github.com/reactphp/http-client/pull/58 + // $this->assertEquals('chunked', $response->getHeaderLine('Transfer-Encoding')); + $this->assertFalse($response->hasHeader('Transfer-Encoding')); + + $this->assertStringStartsWith('{', (string) $response->getBody()); + $this->assertStringEndsWith('}', (string) $response->getBody()); + } + + public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeepsConnectionOpen() + { + $closed = new \React\Promise\Deferred(); + $socket = new \React\Socket\Server(0, $this->loop); + $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) use ($closed) { + $connection->on('data', function () use ($connection) { + $connection->write("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"); + }); + $connection->on('close', function () use ($closed) { + $closed->resolve(true); + }); + }); + + $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; + + $response = Block\await($this->browser->get($this->base . 'get', array()), $this->loop); + $this->assertEquals('hello', (string)$response->getBody()); + + $ret = Block\await($closed->promise(), $this->loop, 0.1); + $this->assertTrue($ret); + + $socket->close(); + } + + public function testPostStreamChunked() + { + $stream = new ThroughStream(); + + $this->loop->addTimer(0.001, function () use ($stream) { + $stream->end('hello world'); + }); + + $response = Block\await($this->browser->post($this->base . 'post', array(), $stream), $this->loop); + $data = json_decode((string)$response->getBody(), true); + + $this->assertEquals('hello world', $data['data']); + $this->assertFalse(isset($data['headers']['Content-Length'])); + $this->assertEquals('chunked', $data['headers']['Transfer-Encoding']); + } + + public function testPostStreamKnownLength() + { + $stream = new ThroughStream(); + + $this->loop->addTimer(0.001, function () use ($stream) { + $stream->end('hello world'); + }); + + $response = Block\await($this->browser->post($this->base . 'post', array('Content-Length' => 11), $stream), $this->loop); + $data = json_decode((string)$response->getBody(), true); + + $this->assertEquals('hello world', $data['data']); + } + + /** + * @doesNotPerformAssertions + */ + public function testPostStreamWillStartSendingRequestEvenWhenBodyDoesNotEmitData() + { + $server = new Server(array(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { + return new Response(200); + })); + $socket = new \React\Socket\Server(0, $this->loop); + $server->listen($socket); + + $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; + + $stream = new ThroughStream(); + Block\await($this->browser->post($this->base . 'post', array(), $stream), $this->loop); + + $socket->close(); + } + + public function testPostStreamClosed() + { + $stream = new ThroughStream(); + $stream->close(); + + $response = Block\await($this->browser->post($this->base . 'post', array(), $stream), $this->loop); + $data = json_decode((string)$response->getBody(), true); + + $this->assertEquals('', $data['data']); + } + + public function testSendsHttp11ByDefault() + { + $server = new Server(function (ServerRequestInterface $request) { + return new Response( + 200, + array(), + $request->getProtocolVersion() + ); + }); + $socket = new \React\Socket\Server(0, $this->loop); + $server->listen($socket); + + $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; + + $response = Block\await($this->browser->get($this->base), $this->loop); + $this->assertEquals('1.1', (string)$response->getBody()); + + $socket->close(); + } + + public function testSendsExplicitHttp10Request() + { + $server = new Server(function (ServerRequestInterface $request) { + return new Response( + 200, + array(), + $request->getProtocolVersion() + ); + }); + $socket = new \React\Socket\Server(0, $this->loop); + $server->listen($socket); + + $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; + + $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base), $this->loop); + $this->assertEquals('1.0', (string)$response->getBody()); + + $socket->close(); + } + + public function testHeadRequestReceivesResponseWithEmptyBodyButWithContentLengthResponseHeader() + { + $response = Block\await($this->browser->head($this->base . 'get'), $this->loop); + $this->assertEquals('', (string)$response->getBody()); + $this->assertEquals(0, $response->getBody()->getSize()); + $this->assertEquals('5', $response->getHeaderLine('Content-Length')); + } + + public function testRequestGetReceivesBufferedResponseEvenWhenStreamingOptionHasBeenTurnedOn() + { + $response = Block\await( + $this->browser->withOptions(array('streaming' => true))->request('GET', $this->base . 'get'), + $this->loop + ); + $this->assertEquals('hello', (string)$response->getBody()); + } + + public function testRequestStreamingGetReceivesStreamingResponseBody() + { + $buffer = Block\await( + $this->browser->requestStreaming('GET', $this->base . 'get')->then(function (ResponseInterface $response) { + return Stream\buffer($response->getBody()); + }), + $this->loop + ); + + $this->assertEquals('hello', $buffer); + } + + public function testRequestStreamingGetReceivesStreamingResponseEvenWhenStreamingOptionHasBeenTurnedOff() + { + $response = Block\await( + $this->browser->withOptions(array('streaming' => false))->requestStreaming('GET', $this->base . 'get'), + $this->loop + ); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $response->getBody()); + $this->assertEquals('', (string)$response->getBody()); + } + + public function testRequestStreamingGetReceivesStreamingResponseBodyEvenWhenResponseBufferExceeded() + { + $buffer = Block\await( + $this->browser->withResponseBuffer(4)->requestStreaming('GET', $this->base . 'get')->then(function (ResponseInterface $response) { + return Stream\buffer($response->getBody()); + }), + $this->loop + ); + + $this->assertEquals('hello', $buffer); + } +} diff --git a/tests/Io/ChunkedEncoderTest.php b/tests/Io/ChunkedEncoderTest.php index 75d43d4a..87ce44c4 100644 --- a/tests/Io/ChunkedEncoderTest.php +++ b/tests/Io/ChunkedEncoderTest.php @@ -22,7 +22,7 @@ public function setUpChunkedStream() public function testChunked() { - $this->chunkedStream->on('data', $this->expectCallableOnce(array("5\r\nhello\r\n"))); + $this->chunkedStream->on('data', $this->expectCallableOnceWith("5\r\nhello\r\n")); $this->input->emit('data', array('hello')); } @@ -34,7 +34,7 @@ public function testEmptyString() public function testBiggerStringToCheckHexValue() { - $this->chunkedStream->on('data', $this->expectCallableOnce(array("1a\r\nabcdefghijklmnopqrstuvwxyz\r\n"))); + $this->chunkedStream->on('data', $this->expectCallableOnceWith("1a\r\nabcdefghijklmnopqrstuvwxyz\r\n")); $this->input->emit('data', array('abcdefghijklmnopqrstuvwxyz')); } diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php new file mode 100644 index 00000000..aaf93ce1 --- /dev/null +++ b/tests/Io/SenderTest.php @@ -0,0 +1,393 @@ +loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + } + + public function testCreateFromLoop() + { + $sender = Sender::createFromLoop($this->loop, null, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $this->assertInstanceOf('React\Http\Io\Sender', $sender); + } + + public function testSenderRejectsInvalidUri() + { + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->never())->method('connect'); + + $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $request = new Request('GET', 'www.google.com'); + + $promise = $sender->send($request); + + $this->setExpectedException('InvalidArgumentException'); + Block\await($promise, $this->loop); + } + + 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), $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $request = new Request('GET', 'http://www.google.com/'); + + $promise = $sender->send($request); + + $this->setExpectedException('RuntimeException'); + Block\await($promise, $this->loop); + } + + public function testSendPostWillAutomaticallySendContentLengthHeader() + { + $client = $this->getMockBuilder('React\HttpClient\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\HttpClient\Request')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $request = new Request('POST', 'http://www.google.com/', array(), 'hello'); + $sender->send($request); + } + + public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmptyRequestBody() + { + $client = $this->getMockBuilder('React\HttpClient\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\HttpClient\Request')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $request = new Request('POST', 'http://www.google.com/', array(), ''); + $sender->send($request); + } + + public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() + { + $outgoing = $this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock(); + $outgoing->expects($this->once())->method('write')->with(""); + + $client = $this->getMockBuilder('React\HttpClient\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); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $stream = new ThroughStream(); + $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $sender->send($request); + } + + public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAndRespectRequestThrottling() + { + $outgoing = $this->getMockBuilder('React\HttpClient\Request')->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); + + $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->willReturn($outgoing); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $stream = new ThroughStream(); + $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $sender->send($request); + + $ret = $stream->write('hello'); + $this->assertFalse($ret); + } + + public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() + { + $outgoing = $this->getMockBuilder('React\HttpClient\Request')->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); + + $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->willReturn($outgoing); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $stream = new ThroughStream(); + $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $sender->send($request); + + $stream->end(); + } + + public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() + { + $outgoing = $this->getMockBuilder('React\HttpClient\Request')->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'); + $outgoing->expects($this->once())->method('close'); + + $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->willReturn($outgoing); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $expected = new \RuntimeException(); + $stream = new ThroughStream(); + $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $promise = $sender->send($request); + + $stream->emit('error', array($expected)); + + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('Request failed because request body reported an error', $exception->getMessage()); + $this->assertSame($expected, $exception->getPrevious()); + } + + public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() + { + $outgoing = $this->getMockBuilder('React\HttpClient\Request')->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'); + $outgoing->expects($this->once())->method('close'); + + $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->willReturn($outgoing); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $stream = new ThroughStream(); + $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $promise = $sender->send($request); + + $stream->close(); + + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('Request failed because request body closed unexpectedly', $exception->getMessage()); + } + + public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() + { + $outgoing = $this->getMockBuilder('React\HttpClient\Request')->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'); + $outgoing->expects($this->never())->method('close'); + + $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->willReturn($outgoing); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $stream = new ThroughStream(); + $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $promise = $sender->send($request); + + $stream->end(); + $stream->close(); + + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertNull($exception); + } + + public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() + { + $client = $this->getMockBuilder('React\HttpClient\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\HttpClient\Request')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $stream = new ThroughStream(); + $request = new Request('POST', 'http://www.google.com/', array('Content-Length' => '100'), new ReadableBodyStream($stream)); + $sender->send($request); + } + + public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() + { + $client = $this->getMockBuilder('React\HttpClient\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\HttpClient\Request')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $request = new Request('GET', 'http://www.google.com/'); + $sender->send($request); + } + + public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyRequestBody() + { + $client = $this->getMockBuilder('React\HttpClient\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\HttpClient\Request')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $request = new Request('CUSTOM', 'http://www.google.com/'); + $sender->send($request); + } + + public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsIs() + { + $client = $this->getMockBuilder('React\HttpClient\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\HttpClient\Request')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $request = new Request('CUSTOM', 'http://www.google.com/', array('Content-Length' => '0')); + $sender->send($request); + } + + public function testCancelRequestWillCancelConnector() + { + $promise = new \React\Promise\Promise(function () { }, function () { + throw new \RuntimeException(); + }); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->willReturn($promise); + + $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $request = new Request('GET', 'http://www.google.com/'); + + $promise = $sender->send($request); + $promise->cancel(); + + $this->setExpectedException('RuntimeException'); + Block\await($promise, $this->loop); + } + + public function testCancelRequestWillCloseConnection() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('close'); + + $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), $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + + $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', + ), + ); + } + + /** + * @dataProvider provideRequestProtocolVersion + */ + public function testRequestProtocolVersion(Request $Request, $method, $uri, $headers, $protocolVersion) + { + $http = $this->getMockBuilder('React\HttpClient\Client') + ->setMethods(array( + 'request', + )) + ->setConstructorArgs(array( + $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(), + ))->getMock(); + + $request = $this->getMockBuilder('React\HttpClient\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, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender->send($Request); + } +} diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php new file mode 100644 index 00000000..882b1860 --- /dev/null +++ b/tests/Io/TransactionTest.php @@ -0,0 +1,861 @@ +makeSenderMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $transaction = new Transaction($sender, new MessageFactory(), $loop); + + $new = $transaction->withOptions(array('followRedirects' => false)); + + $this->assertInstanceOf('React\Http\Io\Transaction', $new); + $this->assertNotSame($transaction, $new); + + $ref = new \ReflectionProperty($new, 'followRedirects'); + $ref->setAccessible(true); + + $this->assertFalse($ref->getValue($new)); + } + + public function testWithOptionsDoesNotChangeOriginalInstance() + { + $sender = $this->makeSenderMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $transaction = new Transaction($sender, new MessageFactory(), $loop); + + $transaction->withOptions(array('followRedirects' => false)); + + $ref = new \ReflectionProperty($transaction, 'followRedirects'); + $ref->setAccessible(true); + + $this->assertTrue($ref->getValue($transaction)); + } + + public function testWithOptionsNullValueReturnsNewInstanceWithDefaultOption() + { + $sender = $this->makeSenderMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $transaction = new Transaction($sender, new MessageFactory(), $loop); + + $transaction = $transaction->withOptions(array('followRedirects' => false)); + $transaction = $transaction->withOptions(array('followRedirects' => null)); + + $ref = new \ReflectionProperty($transaction, 'followRedirects'); + $ref->setAccessible(true); + + $this->assertTrue($ref->getValue($transaction)); + } + + public function testTimeoutExplicitOptionWillStartTimeoutTimer() + { + $messageFactory = new MessageFactory(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 2)); + $promise = $transaction->send($request); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + } + + public function testTimeoutImplicitFromIniWillStartTimeoutTimer() + { + $messageFactory = new MessageFactory(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + + $transaction = new Transaction($sender, $messageFactory, $loop); + + $old = ini_get('default_socket_timeout'); + ini_set('default_socket_timeout', '2'); + $promise = $transaction->send($request); + ini_set('default_socket_timeout', $old); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + } + + public function testTimeoutExplicitOptionWillRejectWhenTimerFires() + { + $messageFactory = new MessageFactory(); + + $timeout = null; + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(2, $this->callback(function ($cb) use (&$timeout) { + $timeout = $cb; + return true; + }))->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 2)); + $promise = $transaction->send($request); + + $this->assertNotNull($timeout); + $timeout(); + + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('Request timed out after 2 seconds', $exception->getMessage()); + } + + public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderResolvesImmediately() + { + $messageFactory = new MessageFactory(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $response = $messageFactory->response(1.0, 200, 'OK', array(), ''); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 0.001)); + $promise = $transaction->send($request); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $promise->then($this->expectCallableOnceWith($response)); + } + + public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderResolvesLaterOn() + { + $messageFactory = new MessageFactory(); + + $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); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $response = $messageFactory->response(1.0, 200, 'OK', array(), ''); + + $deferred = new Deferred(); + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 0.001)); + $promise = $transaction->send($request); + + $deferred->resolve($response); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $promise->then($this->expectCallableOnceWith($response)); + } + + public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderRejectsImmediately() + { + $messageFactory = new MessageFactory(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $exception = new \RuntimeException(); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\reject($exception)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 0.001)); + $promise = $transaction->send($request); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $promise->then(null, $this->expectCallableOnceWith($exception)); + } + + public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderRejectsLaterOn() + { + $messageFactory = new MessageFactory(); + + $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); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + + $deferred = new Deferred(); + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 0.001)); + $promise = $transaction->send($request); + + $exception = new \RuntimeException(); + $deferred->reject($exception); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $promise->then(null, $this->expectCallableOnceWith($exception)); + } + + public function testTimeoutExplicitNegativeWillNotStartTimeoutTimer() + { + $messageFactory = new MessageFactory(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => -1)); + $promise = $transaction->send($request); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + } + + public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenRequestBodyIsStreaming() + { + $messageFactory = new MessageFactory(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $stream = new ThroughStream(); + $request = $messageFactory->request('POST', 'http://example.com/', array(), $stream); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 2)); + $promise = $transaction->send($request); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + } + + public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingRequestBodyIsAlreadyClosed() + { + $messageFactory = new MessageFactory(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $stream = new ThroughStream(); + $stream->close(); + $request = $messageFactory->request('POST', 'http://example.com/', array(), $stream); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 2)); + $promise = $transaction->send($request); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + } + + public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingRequestBodyClosesWhileSenderIsStillPending() + { + $messageFactory = new MessageFactory(); + + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $stream = new ThroughStream(); + $request = $messageFactory->request('POST', 'http://example.com/', array(), $stream); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 2)); + $promise = $transaction->send($request); + + $stream->close(); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + } + + public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenStreamingRequestBodyClosesAfterSenderRejects() + { + $messageFactory = new MessageFactory(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $stream = new ThroughStream(); + $request = $messageFactory->request('POST', 'http://example.com/', array(), $stream); + + $deferred = new Deferred(); + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 2)); + $promise = $transaction->send($request); + + $deferred->reject(new \RuntimeException('Request failed')); + $stream->close(); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + } + + public function testTimeoutExplicitOptionWillRejectWhenTimerFiresAfterStreamingRequestBodyCloses() + { + $messageFactory = new MessageFactory(); + + $timeout = null; + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with(2, $this->callback(function ($cb) use (&$timeout) { + $timeout = $cb; + return true; + }))->willReturn($timer); + $loop->expects($this->never())->method('cancelTimer'); + + $stream = new ThroughStream(); + $request = $messageFactory->request('POST', 'http://example.com/', array(), $stream); + + $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 2)); + $promise = $transaction->send($request); + + $stream->close(); + + $this->assertNotNull($timeout); + $timeout(); + + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('Request timed out after 2 seconds', $exception->getMessage()); + } + + public function testReceivingErrorResponseWillRejectWithResponseException() + { + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $response = new Response(404); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + // 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, new MessageFactory(), $loop); + $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()); + } + } + + public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefault() + { + $messageFactory = new MessageFactory(); + $loop = Factory::create(); + + $stream = new ThroughStream(); + $loop->addTimer(0.001, function () use ($stream) { + $stream->emit('data', array('hello world')); + $stream->close(); + }); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $response = $messageFactory->response(1.0, 200, 'OK', array(), $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, $messageFactory, $loop); + $promise = $transaction->send($request); + + $response = Block\await($promise, $loop); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('hello world', (string)$response->getBody()); + } + + public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBufferWillRejectAndCloseResponseStream() + { + $messageFactory = new MessageFactory(); + $loop = Factory::create(); + + $stream = new ThroughStream(); + $stream->on('close', $this->expectCallableOnce()); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + + $response = $messageFactory->response(1.0, 200, 'OK', array('Content-Length' => '100000000'), $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, $messageFactory, $loop); + $promise = $transaction->send($request); + + $this->setExpectedException('OverflowException'); + Block\await($promise, $loop, 0.001); + } + + public function testCancelBufferingResponseWillCloseStreamAndReject() + { + $messageFactory = new MessageFactory(); + $loop = Factory::create(); + + $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $stream->expects($this->any())->method('isReadable')->willReturn(true); + $stream->expects($this->once())->method('close'); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $response = $messageFactory->response(1.0, 200, 'OK', array(), $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, $messageFactory, $loop); + $promise = $transaction->send($request); + $promise->cancel(); + + $this->setExpectedException('RuntimeException'); + Block\await($promise, $loop, 0.001); + } + + public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStreamingIsEnabled() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $response = $messageFactory->response(1.0, 200, 'OK', array(), $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock()); + + // 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, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('streaming' => true, 'timeout' => -1)); + $promise = $transaction->send($request); + + $response = Block\await($promise, $loop); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('', (string)$response->getBody()); + } + + public function testResponseCode304WithoutLocationWillResolveWithResponseAsIs() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + // conditional GET request will respond with 304 (Not Modified + $request = $messageFactory->request('GET', 'http://example.com/', array('If-None-Match' => '"abc"')); + $response = $messageFactory->response(1.0, 304, null, array('ETag' => '"abc"')); + $sender = $this->makeSenderMock(); + $sender->expects($this->once())->method('send')->with($request)->willReturn(Promise\resolve($response)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => -1)); + $promise = $transaction->send($request); + + $promise->then($this->expectCallableOnceWith($response)); + } + + public function testCustomRedirectResponseCode333WillFollowLocationHeaderAndSendRedirectedRequest() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + // original GET request will respond with custom 333 redirect status code and follow location header + $requestOriginal = $messageFactory->request('GET', 'http://example.com/'); + $response = $messageFactory->response(1.0, 333, null, array('Location' => 'foo')); + $sender = $this->makeSenderMock(); + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($requestOriginal), + array($this->callback(function (RequestInterface $request) { + return $request->getMethod() === 'GET' && (string)$request->getUri() === 'http://example.com/foo'; + })) + )->willReturnOnConsecutiveCalls( + Promise\resolve($response), + new \React\Promise\Promise(function () { }) + ); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction->send($requestOriginal); + } + + public function testFollowingRedirectWithSpecifiedHeaders() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $customHeaders = array('User-Agent' => 'Chrome'); + $requestWithUserAgent = $messageFactory->request('GET', 'http://example.com/', $customHeaders); + $sender = $this->makeSenderMock(); + + // mock sender to resolve promise with the given $redirectResponse in + // response to the given $requestWithUserAgent + $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://redirect.com/')); + $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); + + // mock sender to resolve promise with the given $okResponse in + // response to the given $requestWithUserAgent + $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $that = $this; + $sender->expects($this->at(1)) + ->method('send') + ->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals(array('Chrome'), $request->getHeader('User-Agent')); + return true; + }))->willReturn(Promise\resolve($okResponse)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction->send($requestWithUserAgent); + } + + public function testRemovingAuthorizationHeaderWhenChangingHostnamesDuringRedirect() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $customHeaders = array('Authorization' => 'secret'); + $requestWithAuthorization = $messageFactory->request('GET', 'http://example.com/', $customHeaders); + $sender = $this->makeSenderMock(); + + // mock sender to resolve promise with the given $redirectResponse in + // response to the given $requestWithAuthorization + $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://redirect.com/')); + $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); + + // mock sender to resolve promise with the given $okResponse in + // response to the given $requestWithAuthorization + $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $that = $this; + $sender->expects($this->at(1)) + ->method('send') + ->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertFalse($request->hasHeader('Authorization')); + return true; + }))->willReturn(Promise\resolve($okResponse)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction->send($requestWithAuthorization); + } + + public function testAuthorizationHeaderIsForwardedWhenRedirectingToSameDomain() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $customHeaders = array('Authorization' => 'secret'); + $requestWithAuthorization = $messageFactory->request('GET', 'http://example.com/', $customHeaders); + $sender = $this->makeSenderMock(); + + // mock sender to resolve promise with the given $redirectResponse in + // response to the given $requestWithAuthorization + $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new')); + $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); + + // mock sender to resolve promise with the given $okResponse in + // response to the given $requestWithAuthorization + $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $that = $this; + $sender->expects($this->at(1)) + ->method('send') + ->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals(array('secret'), $request->getHeader('Authorization')); + return true; + }))->willReturn(Promise\resolve($okResponse)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction->send($requestWithAuthorization); + } + + public function testAuthorizationHeaderIsForwardedWhenLocationContainsAuthentication() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $request = $messageFactory->request('GET', 'http://example.com/'); + $sender = $this->makeSenderMock(); + + // mock sender to resolve promise with the given $redirectResponse in + // response to the given $requestWithAuthorization + $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://user:pass@example.com/new')); + $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); + + // mock sender to resolve promise with the given $okResponse in + // response to the given $requestWithAuthorization + $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $that = $this; + $sender->expects($this->at(1)) + ->method('send') + ->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertEquals('user:pass', $request->getUri()->getUserInfo()); + $that->assertFalse($request->hasHeader('Authorization')); + return true; + }))->willReturn(Promise\resolve($okResponse)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction->send($request); + } + + public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $customHeaders = array( + 'Content-Type' => 'text/html; charset=utf-8', + 'Content-Length' => '111', + ); + + $requestWithCustomHeaders = $messageFactory->request('GET', 'http://example.com/', $customHeaders); + $sender = $this->makeSenderMock(); + + // mock sender to resolve promise with the given $redirectResponse in + // response to the given $requestWithCustomHeaders + $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new')); + $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); + + // mock sender to resolve promise with the given $okResponse in + // response to the given $requestWithCustomHeaders + $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $that = $this; + $sender->expects($this->at(1)) + ->method('send') + ->with($this->callback(function (RequestInterface $request) use ($that) { + $that->assertFalse($request->hasHeader('Content-Type')); + $that->assertFalse($request->hasHeader('Content-Length')); + return true; + }))->willReturn(Promise\resolve($okResponse)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction->send($requestWithCustomHeaders); + } + + public function testCancelTransactionWillCancelRequest() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $request = $messageFactory->request('GET', 'http://example.com/'); + $sender = $this->makeSenderMock(); + + $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); + + // mock sender to return pending promise which should be cancelled when cancelling result + $sender->expects($this->once())->method('send')->willReturn($pending); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $promise = $transaction->send($request); + + $promise->cancel(); + } + + public function testCancelTransactionWillCancelTimeoutTimer() + { + $messageFactory = new MessageFactory(); + + $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); + + $request = $messageFactory->request('GET', 'http://example.com/'); + $sender = $this->makeSenderMock(); + + $pending = new \React\Promise\Promise(function () { }, function () { throw new \RuntimeException(); }); + + // mock sender to return pending promise which should be cancelled when cancelling result + $sender->expects($this->once())->method('send')->willReturn($pending); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = $transaction->withOptions(array('timeout' => 2)); + $promise = $transaction->send($request); + + $promise->cancel(); + } + + public function testCancelTransactionWillCancelRedirectedRequest() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $request = $messageFactory->request('GET', 'http://example.com/'); + $sender = $this->makeSenderMock(); + + // mock sender to resolve promise with the given $redirectResponse in + $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new')); + $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); + + $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); + + // mock sender to return pending promise which should be cancelled when cancelling result + $sender->expects($this->at(1))->method('send')->willReturn($pending); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $promise = $transaction->send($request); + + $promise->cancel(); + } + + public function testCancelTransactionWillCancelRedirectedRequestAgain() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $request = $messageFactory->request('GET', 'http://example.com/'); + $sender = $this->makeSenderMock(); + + // mock sender to resolve promise with the given $redirectResponse in + $first = new Deferred(); + $sender->expects($this->at(0))->method('send')->willReturn($first->promise()); + + $second = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); + + // mock sender to return pending promise which should be cancelled when cancelling result + $sender->expects($this->at(1))->method('send')->willReturn($second); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $promise = $transaction->send($request); + + // mock sender to resolve promise with the given $redirectResponse in + $first->resolve($messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new'))); + + $promise->cancel(); + } + + public function testCancelTransactionWillCloseBufferingStream() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $request = $messageFactory->request('GET', 'http://example.com/'); + $sender = $this->makeSenderMock(); + + $body = new ThroughStream(); + $body->on('close', $this->expectCallableOnce()); + + // mock sender to resolve promise with the given $redirectResponse in + $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new'), $body); + $sender->expects($this->once())->method('send')->willReturn(Promise\resolve($redirectResponse)); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $promise = $transaction->send($request); + + $promise->cancel(); + } + + public function testCancelTransactionWillCloseBufferingStreamAgain() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $request = $messageFactory->request('GET', 'http://example.com/'); + $sender = $this->makeSenderMock(); + + $first = new Deferred(); + $sender->expects($this->once())->method('send')->willReturn($first->promise()); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $promise = $transaction->send($request); + + $body = new ThroughStream(); + $body->on('close', $this->expectCallableOnce()); + + // mock sender to resolve promise with the given $redirectResponse in + $first->resolve($messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new'), $body)); + $promise->cancel(); + } + + public function testCancelTransactionShouldCancelSendingPromise() + { + $messageFactory = new MessageFactory(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $request = $messageFactory->request('GET', 'http://example.com/'); + $sender = $this->makeSenderMock(); + + // mock sender to resolve promise with the given $redirectResponse in + $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new')); + $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); + + $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); + + // mock sender to return pending promise which should be cancelled when cancelling result + $sender->expects($this->at(1))->method('send')->willReturn($pending); + + $transaction = new Transaction($sender, $messageFactory, $loop); + $promise = $transaction->send($request); + + $promise->cancel(); + } + + /** + * @return MockObject + */ + private function makeSenderMock() + { + return $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + } +} diff --git a/tests/Message/MessageFactoryTest.php b/tests/Message/MessageFactoryTest.php new file mode 100644 index 00000000..c06418bf --- /dev/null +++ b/tests/Message/MessageFactoryTest.php @@ -0,0 +1,197 @@ +messageFactory = new MessageFactory(); + } + + public function testUriSimple() + { + $uri = $this->messageFactory->uri('http://www.lueck.tv/'); + + $this->assertEquals('http', $uri->getScheme()); + $this->assertEquals('www.lueck.tv', $uri->getHost()); + $this->assertEquals('/', $uri->getPath()); + + $this->assertEquals(null, $uri->getPort()); + $this->assertEquals('', $uri->getQuery()); + } + + public function testUriComplete() + { + $uri = $this->messageFactory->uri('https://example.com:8080/?just=testing'); + + $this->assertEquals('https', $uri->getScheme()); + $this->assertEquals('example.com', $uri->getHost()); + $this->assertEquals(8080, $uri->getPort()); + $this->assertEquals('/', $uri->getPath()); + $this->assertEquals('just=testing', $uri->getQuery()); + } + + public function testPlaceholdersInUriWillBeEscaped() + { + $uri = $this->messageFactory->uri('http://example.com/%7Bversion%7D'); + + $this->assertEquals('/%7Bversion%7D', $uri->getPath()); + } + + public function testEscapedPlaceholdersInUriWillStayEscaped() + { + $uri = $this->messageFactory->uri('http://example.com/%7Bversion%7D'); + + $this->assertEquals('/%7Bversion%7D', $uri->getPath()); + } + + public function testResolveRelative() + { + $base = $this->messageFactory->uri('http://example.com/base/'); + + $this->assertEquals('http://example.com/base/', $this->messageFactory->uriRelative($base, '')); + $this->assertEquals('http://example.com/', $this->messageFactory->uriRelative($base, '/')); + + $this->assertEquals('http://example.com/base/a', $this->messageFactory->uriRelative($base, 'a')); + $this->assertEquals('http://example.com/a', $this->messageFactory->uriRelative($base, '../a')); + } + + public function testResolveAbsolute() + { + $base = $this->messageFactory->uri('http://example.org/'); + + $this->assertEquals('http://www.example.com/', $this->messageFactory->uriRelative($base, 'http://www.example.com/')); + } + + public function testResolveUri() + { + $base = $this->messageFactory->uri('http://example.org/'); + + $this->assertEquals('http://www.example.com/', $this->messageFactory->uriRelative($base, $this->messageFactory->uri('http://www.example.com/'))); + } + + public function testBodyString() + { + $body = $this->messageFactory->body('hi'); + + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertNotInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertEquals(2, $body->getSize()); + $this->assertEquals('hi', (string)$body); + } + + public function testBodyReadableStream() + { + $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $body = $this->messageFactory->body($stream); + + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertEquals(null, $body->getSize()); + $this->assertEquals('', (string)$body); + } + + public function testResponseWithBodyString() + { + $response = $this->messageFactory->response('1.1', 200, 'OK', array(), 'hi'); + + $body = $response->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertNotInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertEquals(2, $body->getSize()); + $this->assertEquals('hi', (string)$body); + } + + public function testResponseWithStreamingBodyHasUnknownSizeByDefault() + { + $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $response = $this->messageFactory->response('1.1', 200, 'OK', array(), $stream); + + $body = $response->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertNull($body->getSize()); + $this->assertEquals('', (string)$body); + } + + public function testResponseWithStreamingBodyHasSizeFromContentLengthHeader() + { + $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $response = $this->messageFactory->response('1.1', 200, 'OK', array('Content-Length' => '100'), $stream); + + $body = $response->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertEquals(100, $body->getSize()); + $this->assertEquals('', (string)$body); + } + + public function testResponseWithStreamingBodyHasUnknownSizeWithTransferEncodingChunkedHeader() + { + $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $response = $this->messageFactory->response('1.1', 200, 'OK', array('Transfer-Encoding' => 'chunked'), $stream); + + $body = $response->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertNull($body->getSize()); + $this->assertEquals('', (string)$body); + } + + public function testResponseWithStreamingBodyHasZeroSizeForInformationalResponse() + { + $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $response = $this->messageFactory->response('1.1', 101, 'OK', array('Content-Length' => '100'), $stream); + + $body = $response->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertEquals(0, $body->getSize()); + $this->assertEquals('', (string)$body); + } + + public function testResponseWithStreamingBodyHasZeroSizeForNoContentResponse() + { + $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $response = $this->messageFactory->response('1.1', 204, 'OK', array('Content-Length' => '100'), $stream); + + $body = $response->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertEquals(0, $body->getSize()); + $this->assertEquals('', (string)$body); + } + + public function testResponseWithStreamingBodyHasZeroSizeForNotModifiedResponse() + { + $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $response = $this->messageFactory->response('1.1', 304, 'OK', array('Content-Length' => '100'), $stream); + + $body = $response->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertEquals(0, $body->getSize()); + $this->assertEquals('', (string)$body); + } + + public function testResponseWithStreamingBodyHasZeroSizeForHeadRequestMethod() + { + $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $response = $this->messageFactory->response('1.1', 200, 'OK', array('Content-Length' => '100'), $stream, 'HEAD'); + + $body = $response->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertEquals(0, $body->getSize()); + $this->assertEquals('', (string)$body); + } +} diff --git a/tests/Message/ReadableBodyStreamTest.php b/tests/Message/ReadableBodyStreamTest.php new file mode 100644 index 00000000..b540b888 --- /dev/null +++ b/tests/Message/ReadableBodyStreamTest.php @@ -0,0 +1,255 @@ +input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $this->stream = new ReadableBodyStream($this->input); + } + + public function testIsReadableIfInputIsReadable() + { + $this->input->expects($this->once())->method('isReadable')->willReturn(true); + + $this->assertTrue($this->stream->isReadable()); + } + + public function testIsEofIfInputIsNotReadable() + { + $this->input->expects($this->once())->method('isReadable')->willReturn(false); + + $this->assertTrue($this->stream->eof()); + } + + public function testCloseWillCloseInputStream() + { + $this->input->expects($this->once())->method('close'); + + $this->stream->close(); + } + + public function testCloseWillEmitCloseEvent() + { + $this->input = new ThroughStream(); + $this->stream = new ReadableBodyStream($this->input); + + $called = 0; + $this->stream->on('close', function () use (&$called) { + ++$called; + }); + + $this->stream->close(); + $this->stream->close(); + + $this->assertEquals(1, $called); + } + + public function testCloseInputWillEmitCloseEvent() + { + $this->input = new ThroughStream(); + $this->stream = new ReadableBodyStream($this->input); + + $called = 0; + $this->stream->on('close', function () use (&$called) { + ++$called; + }); + + $this->input->close(); + $this->input->close(); + + $this->assertEquals(1, $called); + } + + public function testEndInputWillEmitCloseEvent() + { + $this->input = new ThroughStream(); + $this->stream = new ReadableBodyStream($this->input); + + $called = 0; + $this->stream->on('close', function () use (&$called) { + ++$called; + }); + + $this->input->end(); + $this->input->end(); + + $this->assertEquals(1, $called); + } + + public function testEndInputWillEmitErrorEventWhenDataDoesNotReachExpectedLength() + { + $this->input = new ThroughStream(); + $this->stream = new ReadableBodyStream($this->input, 5); + + $called = null; + $this->stream->on('error', function ($e) use (&$called) { + $called = $e; + }); + + $this->input->write('hi'); + $this->input->end(); + + $this->assertInstanceOf('UnderflowException', $called); + $this->assertSame('Unexpected end of response body after 2/5 bytes', $called->getMessage()); + } + + public function testDataEventOnInputWillEmitDataEvent() + { + $this->input = new ThroughStream(); + $this->stream = new ReadableBodyStream($this->input); + + $called = null; + $this->stream->on('data', function ($data) use (&$called) { + $called = $data; + }); + + $this->input->write('hello'); + + $this->assertEquals('hello', $called); + } + + public function testDataEventOnInputWillEmitEndWhenDataReachesExpectedLength() + { + $this->input = new ThroughStream(); + $this->stream = new ReadableBodyStream($this->input, 5); + + $called = null; + $this->stream->on('end', function () use (&$called) { + ++$called; + }); + + $this->input->write('hello'); + + $this->assertEquals(1, $called); + } + + public function testEndEventOnInputWillEmitEndOnlyOnceWhenDataAlreadyReachedExpectedLength() + { + $this->input = new ThroughStream(); + $this->stream = new ReadableBodyStream($this->input, 5); + + $called = null; + $this->stream->on('end', function () use (&$called) { + ++$called; + }); + + $this->input->write('hello'); + $this->input->end(); + + $this->assertEquals(1, $called); + } + + public function testDataEventOnInputWillNotEmitEndWhenDataDoesNotReachExpectedLength() + { + $this->input = new ThroughStream(); + $this->stream = new ReadableBodyStream($this->input, 5); + + $called = null; + $this->stream->on('end', function () use (&$called) { + ++$called; + }); + + $this->input->write('hi'); + + $this->assertNull($called); + } + + public function testPauseWillPauseInputStream() + { + $this->input->expects($this->once())->method('pause'); + + $this->stream->pause(); + } + + public function testResumeWillResumeInputStream() + { + $this->input->expects($this->once())->method('resume'); + + $this->stream->resume(); + } + + public function testPointlessTostringReturnsEmptyString() + { + $this->assertEquals('', (string)$this->stream); + } + + public function testPointlessDetachThrows() + { + $this->setExpectedException('BadMethodCallException'); + $this->stream->detach(); + } + + public function testPointlessGetSizeReturnsNull() + { + $this->assertEquals(null, $this->stream->getSize()); + } + + public function testPointlessTellThrows() + { + $this->setExpectedException('BadMethodCallException'); + $this->stream->tell(); + } + + public function testPointlessIsSeekableReturnsFalse() + { + $this->assertEquals(false, $this->stream->isSeekable()); + } + + public function testPointlessSeekThrows() + { + $this->setExpectedException('BadMethodCallException'); + $this->stream->seek(0); + } + + public function testPointlessRewindThrows() + { + $this->setExpectedException('BadMethodCallException'); + $this->stream->rewind(); + } + + public function testPointlessIsWritableReturnsFalse() + { + $this->assertEquals(false, $this->stream->isWritable()); + } + + public function testPointlessWriteThrows() + { + $this->setExpectedException('BadMethodCallException'); + $this->stream->write(''); + } + + public function testPointlessReadThrows() + { + $this->setExpectedException('BadMethodCallException'); + $this->stream->read(8192); + } + + public function testPointlessGetContentsThrows() + { + $this->setExpectedException('BadMethodCallException'); + $this->stream->getContents(); + } + + public function testPointlessGetMetadataReturnsNullWhenKeyIsGiven() + { + $this->assertEquals(null, $this->stream->getMetadata('unknown')); + } + + public function testPointlessGetMetadataReturnsEmptyArrayWhenNoKeyIsGiven() + { + $this->assertEquals(array(), $this->stream->getMetadata()); + } +} diff --git a/tests/Message/ResponseExceptionTest.php b/tests/Message/ResponseExceptionTest.php new file mode 100644 index 00000000..33eeea9e --- /dev/null +++ b/tests/Message/ResponseExceptionTest.php @@ -0,0 +1,23 @@ +withStatus(404, 'File not found'); + + $e = new ResponseException($response); + + $this->assertEquals(404, $e->getCode()); + $this->assertEquals('HTTP status code 404 (File not found)', $e->getMessage()); + + $this->assertSame($response, $e->getResponse()); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 6295e871..575ac274 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,16 +6,6 @@ class TestCase extends BaseTestCase { - protected function expectCallableExactly($amount) - { - $mock = $this->createCallableMock(); - $mock - ->expects($this->exactly($amount)) - ->method('__invoke'); - - return $mock; - } - protected function expectCallableOnce() { $mock = $this->createCallableMock(); @@ -64,10 +54,10 @@ protected function expectCallableConsecutive($numberOfCalls, array $with) protected function createCallableMock() { if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { - // PHPUnit 10+ + // PHPUnit 9+ return $this->getMockBuilder('stdClass')->addMethods(array('__invoke'))->getMock(); } else { - // legacy PHPUnit 4 - PHPUnit 9 + // legacy PHPUnit 4 - PHPUnit 8 return $this->getMockBuilder('stdClass')->setMethods(array('__invoke'))->getMock(); } } From a71880c803028ca9669f0197d2f0f138ea343d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 3 Jul 2020 22:08:20 +0200 Subject: [PATCH 014/152] Remove deprecated APIs and legacy references --- README.md | 178 ++------------------------------ src/Browser.php | 169 ++++-------------------------- tests/BrowserTest.php | 28 +---- tests/FunctionalBrowserTest.php | 25 +---- 4 files changed, 29 insertions(+), 371 deletions(-) diff --git a/README.md b/README.md index af2fada9..92877f5a 100644 --- a/README.md +++ b/README.md @@ -52,16 +52,12 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [delete()](#delete) * [request()](#request) * [requestStreaming()](#requeststreaming) - * [~~submit()~~](#submit) - * [~~send()~~](#send) * [withTimeout()](#withtimeout) * [withFollowRedirects()](#withfollowredirects) * [withRejectErrorResponse()](#withrejecterrorresponse) * [withBase()](#withbase) * [withProtocolVersion()](#withprotocolversion) * [withResponseBuffer()](#withresponsebuffer) - * [~~withOptions()~~](#withoptions) - * [~~withoutBase()~~](#withoutbase) * [React\Http\Middleware](#reacthttpmiddleware) * [StreamingRequestMiddleware](#streamingrequestmiddleware) * [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) @@ -69,7 +65,6 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht * [RequestBodyParserMiddleware](#requestbodyparsermiddleware) * [ResponseInterface](#responseinterface) * [RequestInterface](#requestinterface) - * [UriInterface](#uriinterface) * [ResponseException](#responseexception) * [Install](#install) * [Tests](#tests) @@ -118,8 +113,6 @@ See also the [examples](examples). ### Request methods - - Most importantly, this project provides a [`Browser`](#browser) object that offers several methods that resemble the HTTP protocol methods: @@ -450,8 +443,6 @@ more details. ### Streaming response - - All of the above examples assume you want to store the whole response body in memory. This is easy to get started and works reasonably well for smaller responses. @@ -558,10 +549,6 @@ $stream->on('data', function ($data) { See also the [`requestStreaming()`](#requeststreaming) method for more details. -> Legacy info: Legacy versions prior to v2.9.0 used the legacy - [`streaming` option](#withoptions). This option is now deprecated but otherwise - continues to show the exact same behavior. - ### Streaming request Besides streaming the response body, you can also stream the request body. @@ -1105,8 +1092,6 @@ header or when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. #### Streaming incoming request - - If you're using the advanced [`StreamingRequestMiddleware`](#streamingrequestmiddleware), the request object will be processed once the request headers have been received. This means that this happens irrespective of (i.e. *before*) receiving the @@ -1413,8 +1398,6 @@ If a promise is resolved after the client closes, it will simply be ignored. #### Streaming outgoing response - - The `Response` class in this project supports to add an instance which implements the [ReactPHP ReadableStreamInterface](https://github.com/reactphp/stream#readablestreaminterface) for the response body. @@ -1859,7 +1842,7 @@ $browser = new React\Http\Browser($loop, $connector); #### get() -The `get(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to +The `get(string $url, array $headers = array()): PromiseInterface` method can be used to send an HTTP GET request. ```php @@ -1870,13 +1853,9 @@ $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response See also [example 01](examples/01-google.php). -> For BC reasons, this method accepts the `$url` as either a `string` - value or as an `UriInterface`. It's recommended to explicitly cast any - objects implementing `UriInterface` to `string`. - #### post() -The `post(string|UriInterface $url, array $headers = array(), string|ReadableStreamInterface $contents = ''): PromiseInterface` method can be used to +The `post(string $url, array $headers = array(), string|ReadableStreamInterface $contents = ''): PromiseInterface` method can be used to send an HTTP POST request. ```php @@ -1925,13 +1904,9 @@ $loop->addTimer(1.0, function () use ($body) { $browser->post($url, array('Content-Length' => '11'), $body); ``` -> For BC reasons, this method accepts the `$url` as either a `string` - value or as an `UriInterface`. It's recommended to explicitly cast any - objects implementing `UriInterface` to `string`. - #### head() -The `head(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to +The `head(string $url, array $headers = array()): PromiseInterface` method can be used to send an HTTP HEAD request. ```php @@ -1940,13 +1915,9 @@ $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $respons }); ``` -> For BC reasons, this method accepts the `$url` as either a `string` - value or as an `UriInterface`. It's recommended to explicitly cast any - objects implementing `UriInterface` to `string`. - #### patch() -The `patch(string|UriInterface $url, array $headers = array(), string|ReadableStreamInterface $contents = ''): PromiseInterface` method can be used to +The `patch(string $url, array $headers = array(), string|ReadableStreamInterface $contents = ''): PromiseInterface` method can be used to send an HTTP PATCH request. ```php @@ -1976,13 +1947,9 @@ $loop->addTimer(1.0, function () use ($body) { $browser->patch($url, array('Content-Length' => '11'), $body); ``` -> For BC reasons, this method accepts the `$url` as either a `string` - value or as an `UriInterface`. It's recommended to explicitly cast any - objects implementing `UriInterface` to `string`. - #### put() -The `put(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to +The `put(string $url, array $headers = array()): PromiseInterface` method can be used to send an HTTP PUT request. ```php @@ -2014,13 +1981,9 @@ $loop->addTimer(1.0, function () use ($body) { $browser->put($url, array('Content-Length' => '11'), $body); ``` -> For BC reasons, this method accepts the `$url` as either a `string` - value or as an `UriInterface`. It's recommended to explicitly cast any - objects implementing `UriInterface` to `string`. - #### delete() -The `delete(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to +The `delete(string $url, array $headers = array()): PromiseInterface` method can be used to send an HTTP DELETE request. ```php @@ -2029,10 +1992,6 @@ $browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $respo }); ``` -> For BC reasons, this method accepts the `$url` as either a `string` - value or as an `UriInterface`. It's recommended to explicitly cast any - objects implementing `UriInterface` to `string`. - #### request() The `request(string $method, string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to @@ -2070,12 +2029,6 @@ $loop->addTimer(1.0, function () use ($body) { $browser->request('POST', $url, array('Content-Length' => '11'), $body); ``` -> Note that this method is available as of v2.9.0 and always buffers the - response body before resolving. - It does not respect the deprecated [`streaming` option](#withoptions). - If you want to stream the response body, you can use the - [`requestStreaming()`](#requeststreaming) method instead. - #### requestStreaming() The `requestStreaming(string $method, string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to @@ -2138,55 +2091,6 @@ $loop->addTimer(1.0, function () use ($body) { $browser->requestStreaming('POST', $url, array('Content-Length' => '11'), $body); ``` -> Note that this method is available as of v2.9.0 and always resolves the - response without buffering the response body. - It does not respect the deprecated [`streaming` option](#withoptions). - If you want to buffer the response body, use can use the - [`request()`](#request) method instead. - -#### ~~submit()~~ - -> Deprecated since v2.9.0, see [`post()`](#post) instead. - -The deprecated `submit(string|UriInterface $url, array $fields, array $headers = array(), string $method = 'POST'): PromiseInterface` method can be used to -submit an array of field values similar to submitting a form (`application/x-www-form-urlencoded`). - -```php -// deprecated: see post() instead -$browser->submit($url, array('user' => 'test', 'password' => 'secret')); -``` - -> For BC reasons, this method accepts the `$url` as either a `string` - value or as an `UriInterface`. It's recommended to explicitly cast any - objects implementing `UriInterface` to `string`. - -#### ~~send()~~ - -> Deprecated since v2.9.0, see [`request()`](#request) instead. - -The deprecated `send(RequestInterface $request): PromiseInterface` method can be used to -send an arbitrary instance implementing the [`RequestInterface`](#requestinterface) (PSR-7). - -The preferred way to send an HTTP request is by using the above -[request methods](#request-methods), for example the [`get()`](#get) -method to send an HTTP `GET` request. - -As an alternative, if you want to use a custom HTTP request method, you -can use this method: - -```php -$request = new Request('OPTIONS', $url); - -// deprecated: see request() instead -$browser->send($request)->then(…); -``` - -This method will automatically add a matching `Content-Length` request -header if the size of the outgoing request body is known and non-empty. -For an empty request body, if will only include a `Content-Length: 0` -request header if the request method usually expects a request body (only -applies to `POST`, `PUT` and `PATCH`). - #### withTimeout() The `withTimeout(bool|number $timeout): Browser` method can be used to @@ -2313,7 +2217,7 @@ given setting applied. #### withBase() -The `withBase(string|null|UriInterface $baseUrl): Browser` method can be used to +The `withBase(string|null $baseUrl): Browser` method can be used to change the base URL used to resolve relative URLs to. If you configure a base URL, any requests to relative URLs will be @@ -2346,14 +2250,6 @@ This method will throw an `InvalidArgumentException` if the given Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withBase()` method actually returns a *new* [`Browser`](#browser) instance with the given base URL applied. -> For BC reasons, this method accepts the `$baseUrl` as either a `string` - value or as an `UriInterface`. It's recommended to explicitly cast any - objects implementing `UriInterface` to `string`. - -> Changelog: As of v2.9.0 this method accepts a `null` value to reset the - base URL. Earlier versions had to use the deprecated `withoutBase()` - method to reset the base URL. - #### withProtocolVersion() The `withProtocolVersion(string $protocolVersion): Browser` method can be used to @@ -2415,54 +2311,6 @@ 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. -#### ~~withOptions()~~ - -> Deprecated since v2.9.0, see [`withTimeout()`](#withtimeout), [`withFollowRedirects()`](#withfollowredirects) - and [`withRejectErrorResponse()`](#withrejecterrorresponse) instead. - -The deprecated `withOptions(array $options): Browser` method can be used to -change the options to use: - -The [`Browser`](#browser) class exposes several options for the handling of -HTTP transactions. These options resemble some of PHP's -[HTTP context options](https://www.php.net/manual/en/context.http.php) and -can be controlled via the following API (and their defaults): - -```php -// deprecated -$newBrowser = $browser->withOptions(array( - 'timeout' => null, // see withTimeout() instead - 'followRedirects' => true, // see withFollowRedirects() instead - 'maxRedirects' => 10, // see withFollowRedirects() instead - 'obeySuccessCode' => true, // see withRejectErrorResponse() instead - 'streaming' => false, // deprecated, see requestStreaming() instead -)); -``` - -See also [timeouts](#timeouts), [redirects](#redirects) and -[streaming](#streaming-response) for more details. - -Notice that the [`Browser`](#browser) is an immutable object, i.e. this -method actually returns a *new* [`Browser`](#browser) instance with the -options applied. - -#### ~~withoutBase()~~ - -> Deprecated since v2.9.0, see [`withBase()`](#withbase) instead. - -The deprecated `withoutBase(): Browser` method can be used to -remove the base URL. - -```php -// deprecated: see withBase() instead -$newBrowser = $browser->withoutBase(); -``` - -Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withoutBase()` method -actually returns a *new* [`Browser`](#browser) instance without any base URL applied. - -See also [`withBase()`](#withbase). - ### React\Http\Middleware #### StreamingRequestMiddleware @@ -2769,18 +2617,6 @@ This is a standard interface defined in which in turn extends the [`MessageInterface` definition](https://www.php-fig.org/psr/psr-7/#3-1-psr-http-message-messageinterface). -### UriInterface - -The `Psr\Http\Message\UriInterface` represents an absolute or relative URI (aka URL). - -This is a standard interface defined in -[PSR-7: HTTP message interfaces](https://www.php-fig.org/psr/psr-7/), see its -[`UriInterface` definition](https://www.php-fig.org/psr/psr-7/#3-5-psr-http-message-uriinterface). - -> For BC reasons, the request methods accept the URL as either a `string` - value or as an `UriInterface`. It's recommended to explicitly cast any - objects implementing `UriInterface` to `string`. - ### ResponseException The `ResponseException` is an `Exception` sub-class that will be used to reject diff --git a/src/Browser.php b/src/Browser.php index 70e875a2..38479c86 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -75,12 +75,8 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector = * * See also [example 01](../examples/01-google.php). * - * > For BC reasons, this method accepts the `$url` as either a `string` - * value or as an `UriInterface`. It's recommended to explicitly cast any - * objects implementing `UriInterface` to `string`. - * - * @param string|UriInterface $url URL for the request. - * @param array $headers + * @param string $url URL for the request. + * @param array $headers * @return PromiseInterface */ public function get($url, array $headers = array()) @@ -137,11 +133,7 @@ public function get($url, array $headers = array()) * $browser->post($url, array('Content-Length' => '11'), $body); * ``` * - * > For BC reasons, this method accepts the `$url` as either a `string` - * value or as an `UriInterface`. It's recommended to explicitly cast any - * objects implementing `UriInterface` to `string`. - * - * @param string|UriInterface $url URL for the request. + * @param string $url URL for the request. * @param array $headers * @param string|ReadableStreamInterface $contents * @return PromiseInterface @@ -160,12 +152,8 @@ public function post($url, array $headers = array(), $contents = '') * }); * ``` * - * > For BC reasons, this method accepts the `$url` as either a `string` - * value or as an `UriInterface`. It's recommended to explicitly cast any - * objects implementing `UriInterface` to `string`. - * - * @param string|UriInterface $url URL for the request. - * @param array $headers + * @param string $url URL for the request. + * @param array $headers * @return PromiseInterface */ public function head($url, array $headers = array()) @@ -203,11 +191,7 @@ public function head($url, array $headers = array()) * $browser->patch($url, array('Content-Length' => '11'), $body); * ``` * - * > For BC reasons, this method accepts the `$url` as either a `string` - * value or as an `UriInterface`. It's recommended to explicitly cast any - * objects implementing `UriInterface` to `string`. - * - * @param string|UriInterface $url URL for the request. + * @param string $url URL for the request. * @param array $headers * @param string|ReadableStreamInterface $contents * @return PromiseInterface @@ -249,11 +233,7 @@ public function patch($url, array $headers = array(), $contents = '') * $browser->put($url, array('Content-Length' => '11'), $body); * ``` * - * > For BC reasons, this method accepts the `$url` as either a `string` - * value or as an `UriInterface`. It's recommended to explicitly cast any - * objects implementing `UriInterface` to `string`. - * - * @param string|UriInterface $url URL for the request. + * @param string $url URL for the request. * @param array $headers * @param string|ReadableStreamInterface $contents * @return PromiseInterface @@ -272,11 +252,7 @@ public function put($url, array $headers = array(), $contents = '') * }); * ``` * - * > For BC reasons, this method accepts the `$url` as either a `string` - * value or as an `UriInterface`. It's recommended to explicitly cast any - * objects implementing `UriInterface` to `string`. - * - * @param string|UriInterface $url URL for the request. + * @param string $url URL for the request. * @param array $headers * @param string|ReadableStreamInterface $contents * @return PromiseInterface @@ -321,18 +297,11 @@ public function delete($url, array $headers = array(), $contents = '') * $browser->request('POST', $url, array('Content-Length' => '11'), $body); * ``` * - * > Note that this method is available as of v2.9.0 and always buffers the - * response body before resolving. - * It does not respect the deprecated [`streaming` option](#withoptions). - * If you want to stream the response body, you can use the - * [`requestStreaming()`](#requeststreaming) method instead. - * * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. * @param string $url URL for the request * @param array $headers Additional request headers * @param string|ReadableStreamInterface $body HTTP request body contents * @return PromiseInterface - * @since 2.9.0 */ public function request($method, $url, array $headers = array(), $body = '') { @@ -399,93 +368,17 @@ public function request($method, $url, array $headers = array(), $body = '') * $browser->requestStreaming('POST', $url, array('Content-Length' => '11'), $body); * ``` * - * > Note that this method is available as of v2.9.0 and always resolves the - * response without buffering the response body. - * It does not respect the deprecated [`streaming` option](#withoptions). - * If you want to buffer the response body, use can use the - * [`request()`](#request) method instead. - * * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. * @param string $url URL for the request * @param array $headers Additional request headers * @param string|ReadableStreamInterface $body HTTP request body contents * @return PromiseInterface - * @since 2.9.0 */ public function requestStreaming($method, $url, $headers = array(), $contents = '') { return $this->withOptions(array('streaming' => true))->requestMayBeStreaming($method, $url, $headers, $contents); } - /** - * [Deprecated] Submits an array of field values similar to submitting a form (`application/x-www-form-urlencoded`). - * - * ```php - * // deprecated: see post() instead - * $browser->submit($url, array('user' => 'test', 'password' => 'secret')); - * ``` - * - * This method will automatically add a matching `Content-Length` request - * header for the encoded length of the given `$fields`. - * - * > For BC reasons, this method accepts the `$url` as either a `string` - * value or as an `UriInterface`. It's recommended to explicitly cast any - * objects implementing `UriInterface` to `string`. - * - * @param string|UriInterface $url URL for the request. - * @param array $fields - * @param array $headers - * @param string $method - * @return PromiseInterface - * @deprecated 2.9.0 See self::post() instead. - * @see self::post() - */ - public function submit($url, array $fields, $headers = array(), $method = 'POST') - { - $headers['Content-Type'] = 'application/x-www-form-urlencoded'; - $contents = http_build_query($fields); - - return $this->requestMayBeStreaming($method, $url, $headers, $contents); - } - - /** - * [Deprecated] Sends an arbitrary instance implementing the [`RequestInterface`](#requestinterface) (PSR-7). - * - * The preferred way to send an HTTP request is by using the above - * [request methods](#request-methods), for example the [`get()`](#get) - * method to send an HTTP `GET` request. - * - * As an alternative, if you want to use a custom HTTP request method, you - * can use this method: - * - * ```php - * $request = new Request('OPTIONS', $url); - * - * // deprecated: see request() instead - * $browser->send($request)->then(…); - * ``` - * - * This method will automatically add a matching `Content-Length` request - * header if the size of the outgoing request body is known and non-empty. - * For an empty request body, if will only include a `Content-Length: 0` - * request header if the request method usually expects a request body (only - * applies to `POST`, `PUT` and `PATCH`). - * - * @param RequestInterface $request - * @return PromiseInterface - * @deprecated 2.9.0 See self::request() instead. - * @see self::request() - */ - public function send(RequestInterface $request) - { - if ($this->baseUrl !== null) { - // ensure we're actually below the base URL - $request = $request->withUri($this->messageFactory->expandBase($request->getUri(), $this->baseUrl)); - } - - return $this->transaction->send($request); - } - /** * Changes the maximum timeout used for waiting for pending requests. * @@ -676,15 +569,7 @@ public function withRejectErrorResponse($obeySuccessCode) * Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withBase()` method * actually returns a *new* [`Browser`](#browser) instance with the given base URL applied. * - * > For BC reasons, this method accepts the `$baseUrl` as either a `string` - * value or as an `UriInterface`. It's recommended to explicitly cast any - * objects implementing `UriInterface` to `string`. - * - * > Changelog: As of v2.9.0 this method accepts a `null` value to reset the - * base URL. Earlier versions had to use the deprecated `withoutBase()` - * method to reset the base URL. - * - * @param string|null|UriInterface $baseUrl absolute base URL + * @param string|null $baseUrl absolute base URL * @return self * @throws InvalidArgumentException if the given $baseUrl is not a valid absolute URL * @see self::withoutBase() @@ -730,7 +615,6 @@ public function withBase($baseUrl) * @param string $protocolVersion HTTP protocol version to use, must be one of "1.1" or "1.0" * @return self * @throws InvalidArgumentException - * @since 2.8.0 */ public function withProtocolVersion($protocolVersion) { @@ -791,7 +675,7 @@ public function withResponseBuffer($maximumSize) } /** - * [Deprecated] Changes the [options](#options) to use: + * Changes the [options](#options) to use: * * The [`Browser`](#browser) class exposes several options for the handling of * HTTP transactions. These options resemble some of PHP's @@ -818,12 +702,11 @@ public function withResponseBuffer($maximumSize) * * @param array $options * @return self - * @deprecated 2.9.0 See self::withTimeout(), self::withFollowRedirects() and self::withRejectErrorResponse() instead. * @see self::withTimeout() * @see self::withFollowRedirects() * @see self::withRejectErrorResponse() */ - public function withOptions(array $options) + private function withOptions(array $options) { $browser = clone $this; $browser->transaction = $this->transaction->withOptions($options); @@ -831,28 +714,6 @@ public function withOptions(array $options) return $browser; } - /** - * [Deprecated] Removes the base URL. - * - * ```php - * // deprecated: see withBase() instead - * $newBrowser = $browser->withoutBase(); - * ``` - * - * Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withoutBase()` method - * actually returns a *new* [`Browser`](#browser) instance without any base URL applied. - * - * See also [`withBase()`](#withbase). - * - * @return self - * @deprecated 2.9.0 See self::withBase() instead. - * @see self::withBase() - */ - public function withoutBase() - { - return $this->withBase(null); - } - /** * @param string $method * @param string|UriInterface $url @@ -862,6 +723,12 @@ public function withoutBase() */ private function requestMayBeStreaming($method, $url, array $headers = array(), $contents = '') { - return $this->send($this->messageFactory->request($method, $url, $headers, $contents, $this->protocolVersion)); + $request = $this->messageFactory->request($method, $url, $headers, $contents, $this->protocolVersion); + if ($this->baseUrl !== null) { + // ensure we're actually below the base URL + $request = $request->withUri($this->messageFactory->expandBase($request->getUri(), $this->baseUrl)); + } + + return $this->transaction->send($request); } } diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index 56a28303..88ef107e 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -120,19 +120,6 @@ public function testRequestStreamingGetSendsGetRequestWithStreamingExplicitlyEna $this->browser->requestStreaming('GET', 'http://example.com/'); } - public function testSubmitSendsPostRequest() - { - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('POST', $request->getMethod()); - $that->assertEquals('application/x-www-form-urlencoded', $request->getHeaderLine('Content-Type')); - $that->assertEquals('', (string)$request->getBody()); - return true; - }))->willReturn(new Promise(function () { })); - - $this->browser->submit('http://example.com/', array()); - } - public function testWithTimeoutTrueSetsDefaultTimeoutOption() { $this->sender->expects($this->once())->method('withOptions')->with(array('timeout' => null))->willReturnSelf(); @@ -356,7 +343,7 @@ public function testWithBaseUrlInvalidSchemeFails() public function testWithoutBaseFollowedByGetRequestTriesToSendIncompleteRequestUrl() { - $this->browser = $this->browser->withBase('http://example.com/')->withoutBase(); + $this->browser = $this->browser->withBase('http://example.com/')->withBase(null); $that = $this; $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { @@ -380,19 +367,6 @@ public function testWithProtocolVersionFollowedByGetRequestSendsRequestWithProto $this->browser->get('http://example.com/'); } - public function testWithProtocolVersionFollowedBySubmitRequestSendsRequestWithProtocolVersion() - { - $this->browser = $this->browser->withProtocolVersion('1.0'); - - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('1.0', $request->getProtocolVersion()); - return true; - }))->willReturn(new Promise(function () { })); - - $this->browser->submit('http://example.com/', array()); - } - public function testWithProtocolVersionInvalidThrows() { $this->setExpectedException('InvalidArgumentException'); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 16293fbb..c4bbe523 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -180,9 +180,9 @@ public function testCancelGetRequestWillRejectRequest() Block\await($promise, $this->loop); } - public function testCancelSendWithPromiseFollowerWillRejectRequest() + public function testCancelRequestWithPromiseFollowerWillRejectRequest() { - $promise = $this->browser->send(new Request('GET', $this->base . 'get'))->then(function () { + $promise = $this->browser->request('GET', $this->base . 'get')->then(function () { var_dump('noop'); }); $promise->cancel(); @@ -455,7 +455,7 @@ public function testReceiveStreamUntilConnectionsEndsForHttp10() public function testReceiveStreamChunkedForHttp11() { - $response = Block\await($this->browser->send(new Request('GET', $this->base . 'stream/1', array(), null, '1.1')), $this->loop); + $response = Block\await($this->browser->request('GET', $this->base . 'stream/1'), $this->loop); $this->assertEquals('1.1', $response->getProtocolVersion()); @@ -600,15 +600,6 @@ public function testHeadRequestReceivesResponseWithEmptyBodyButWithContentLength $this->assertEquals('5', $response->getHeaderLine('Content-Length')); } - public function testRequestGetReceivesBufferedResponseEvenWhenStreamingOptionHasBeenTurnedOn() - { - $response = Block\await( - $this->browser->withOptions(array('streaming' => true))->request('GET', $this->base . 'get'), - $this->loop - ); - $this->assertEquals('hello', (string)$response->getBody()); - } - public function testRequestStreamingGetReceivesStreamingResponseBody() { $buffer = Block\await( @@ -621,16 +612,6 @@ public function testRequestStreamingGetReceivesStreamingResponseBody() $this->assertEquals('hello', $buffer); } - public function testRequestStreamingGetReceivesStreamingResponseEvenWhenStreamingOptionHasBeenTurnedOff() - { - $response = Block\await( - $this->browser->withOptions(array('streaming' => false))->requestStreaming('GET', $this->base . 'get'), - $this->loop - ); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $response->getBody()); - $this->assertEquals('', (string)$response->getBody()); - } - public function testRequestStreamingGetReceivesStreamingResponseBodyEvenWhenResponseBufferExceeded() { $buffer = Block\await( From a7c1585306e16cc6826c11c7bf896946d3bab05f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 3 Jul 2020 22:52:24 +0200 Subject: [PATCH 015/152] Import react/http-client v0.5.10 Change namespace from `React\HttpClient` to `React\Http\Client` and mark all classes as internal only. See https://github.com/reactphp/http-client for original repo. --- composer.json | 1 - src/Client/ChunkedStreamDecoder.php | 207 ++++++ src/Client/Client.php | 31 + src/Client/Request.php | 295 +++++++++ src/Client/RequestData.php | 128 ++++ src/Client/Response.php | 175 +++++ src/Io/Sender.php | 4 +- tests/Client/DecodeChunkedStreamTest.php | 227 +++++++ tests/Client/FunctionalIntegrationTest.php | 170 +++++ tests/Client/RequestDataTest.php | 154 +++++ tests/Client/RequestTest.php | 714 +++++++++++++++++++++ tests/Client/ResponseTest.php | 168 +++++ tests/Io/SenderTest.php | 58 +- 13 files changed, 2300 insertions(+), 32 deletions(-) create mode 100644 src/Client/ChunkedStreamDecoder.php create mode 100644 src/Client/Client.php create mode 100644 src/Client/Request.php create mode 100644 src/Client/RequestData.php create mode 100644 src/Client/Response.php create mode 100644 tests/Client/DecodeChunkedStreamTest.php create mode 100644 tests/Client/FunctionalIntegrationTest.php create mode 100644 tests/Client/RequestDataTest.php create mode 100644 tests/Client/RequestTest.php create mode 100644 tests/Client/ResponseTest.php diff --git a/composer.json b/composer.json index 755e5d82..50afc4db 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,6 @@ "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "psr/http-message": "^1.0", "react/event-loop": "^1.0 || ^0.5", - "react/http-client": "^0.5.10", "react/promise": "^2.3 || ^1.2.1", "react/promise-stream": "^1.1", "react/socket": "^1.1", diff --git a/src/Client/ChunkedStreamDecoder.php b/src/Client/ChunkedStreamDecoder.php new file mode 100644 index 00000000..02cab52a --- /dev/null +++ b/src/Client/ChunkedStreamDecoder.php @@ -0,0 +1,207 @@ +stream = $stream; + $this->stream->on('data', array($this, 'handleData')); + $this->stream->on('end', array($this, 'handleEnd')); + Util::forwardEvents($this->stream, $this, array( + 'error', + )); + } + + /** @internal */ + public function handleData($data) + { + $this->buffer .= $data; + + do { + $bufferLength = strlen($this->buffer); + $continue = $this->iterateBuffer(); + $iteratedBufferLength = strlen($this->buffer); + } while ( + $continue && + $bufferLength !== $iteratedBufferLength && + $iteratedBufferLength > 0 + ); + + if ($this->buffer === false) { + $this->buffer = ''; + } + } + + protected function iterateBuffer() + { + if (strlen($this->buffer) <= 1) { + return false; + } + + if ($this->nextChunkIsLength) { + $crlfPosition = strpos($this->buffer, static::CRLF); + if ($crlfPosition === false && strlen($this->buffer) > 1024) { + $this->emit('error', array( + new Exception('Chunk length header longer then 1024 bytes'), + )); + $this->close(); + return false; + } + if ($crlfPosition === false) { + return false; // Chunk header hasn't completely come in yet + } + $lengthChunk = substr($this->buffer, 0, $crlfPosition); + if (strpos($lengthChunk, ';') !== false) { + list($lengthChunk) = explode(';', $lengthChunk, 2); + } + if ($lengthChunk !== '') { + $lengthChunk = ltrim(trim($lengthChunk), "0"); + if ($lengthChunk === '') { + // We've reached the end of the stream + $this->reachedEnd = true; + $this->emit('end'); + $this->close(); + return false; + } + } + $this->nextChunkIsLength = false; + if (dechex(@hexdec($lengthChunk)) !== strtolower($lengthChunk)) { + $this->emit('error', array( + new Exception('Unable to validate "' . $lengthChunk . '" as chunk length header'), + )); + $this->close(); + return false; + } + $this->remainingLength = hexdec($lengthChunk); + $this->buffer = substr($this->buffer, $crlfPosition + 2); + return true; + } + + if ($this->remainingLength > 0) { + $chunkLength = $this->getChunkLength(); + if ($chunkLength === 0) { + return true; + } + $this->emit('data', array( + substr($this->buffer, 0, $chunkLength), + $this + )); + $this->remainingLength -= $chunkLength; + $this->buffer = substr($this->buffer, $chunkLength); + return true; + } + + $this->nextChunkIsLength = true; + $this->buffer = substr($this->buffer, 2); + return true; + } + + protected function getChunkLength() + { + $bufferLength = strlen($this->buffer); + + if ($bufferLength >= $this->remainingLength) { + return $this->remainingLength; + } + + return $bufferLength; + } + + public function pause() + { + $this->stream->pause(); + } + + public function resume() + { + $this->stream->resume(); + } + + public function isReadable() + { + return $this->stream->isReadable(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + $this->closed = true; + return $this->stream->close(); + } + + /** @internal */ + public function handleEnd() + { + $this->handleData(''); + + if ($this->closed) { + return; + } + + if ($this->buffer === '' && $this->reachedEnd) { + $this->emit('end'); + $this->close(); + return; + } + + $this->emit( + 'error', + array( + new Exception('Stream ended with incomplete control code') + ) + ); + $this->close(); + } +} diff --git a/src/Client/Client.php b/src/Client/Client.php new file mode 100644 index 00000000..f28ec289 --- /dev/null +++ b/src/Client/Client.php @@ -0,0 +1,31 @@ +connector = $connector; + } + + public function request($method, $url, array $headers = array(), $protocolVersion = '1.0') + { + $requestData = new RequestData($method, $url, $headers, $protocolVersion); + + return new Request($this->connector, $requestData); + } +} diff --git a/src/Client/Request.php b/src/Client/Request.php new file mode 100644 index 00000000..7ebb627f --- /dev/null +++ b/src/Client/Request.php @@ -0,0 +1,295 @@ +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 { + list($response, $bodyChunk) = $this->parseResponse($this->buffer); + } 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; + } + + $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 + )); + }); + + $this->emit('response', array($response, $this)); + + $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 parseResponse($data) + { + $psrResponse = gPsr\parse_response($data); + $headers = array_map(function($val) { + if (1 === count($val)) { + $val = $val[0]; + } + + return $val; + }, $psrResponse->getHeaders()); + + $factory = $this->getResponseFactory(); + + $response = $factory( + 'HTTP', + $psrResponse->getProtocolVersion(), + $psrResponse->getStatusCode(), + $psrResponse->getReasonPhrase(), + $headers + ); + + return array($response, (string)($psrResponse->getBody())); + } + + 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); + } + + public function setResponseFactory($factory) + { + $this->responseFactory = $factory; + } + + public function getResponseFactory() + { + if (null === $factory = $this->responseFactory) { + $stream = $this->stream; + + $factory = function ($protocol, $version, $code, $reasonPhrase, $headers) use ($stream) { + return new Response( + $stream, + $protocol, + $version, + $code, + $reasonPhrase, + $headers + ); + }; + + $this->responseFactory = $factory; + } + + return $factory; + } +} diff --git a/src/Client/RequestData.php b/src/Client/RequestData.php new file mode 100644 index 00000000..55efaa9b --- /dev/null +++ b/src/Client/RequestData.php @@ -0,0 +1,128 @@ +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' => 'React/alpha', + ), + $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/Client/Response.php b/src/Client/Response.php new file mode 100644 index 00000000..be19eb4c --- /dev/null +++ b/src/Client/Response.php @@ -0,0 +1,175 @@ +stream = $stream; + $this->protocol = $protocol; + $this->version = $version; + $this->code = $code; + $this->reasonPhrase = $reasonPhrase; + $this->headers = $headers; + + if (strtolower($this->getHeaderLine('Transfer-Encoding')) === 'chunked') { + $this->stream = new ChunkedStreamDecoder($stream); + $this->removeHeader('Transfer-Encoding'); + } + + $this->stream->on('data', array($this, 'handleData')); + $this->stream->on('error', array($this, 'handleError')); + $this->stream->on('end', array($this, 'handleEnd')); + $this->stream->on('close', array($this, 'handleClose')); + } + + public function getProtocol() + { + return $this->protocol; + } + + public function getVersion() + { + return $this->version; + } + + public function getCode() + { + return $this->code; + } + + public function getReasonPhrase() + { + return $this->reasonPhrase; + } + + 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) + { + if ($this->readable) { + $this->emit('data', array($data)); + } + } + + /** @internal */ + public function handleEnd() + { + if (!$this->readable) { + return; + } + $this->emit('end'); + $this->close(); + } + + /** @internal */ + public function handleError(\Exception $error) + { + if (!$this->readable) { + return; + } + $this->emit('error', array(new \RuntimeException( + "An error occurred in the underlying stream", + 0, + $error + ))); + + $this->close(); + } + + /** @internal */ + public function handleClose() + { + $this->close(); + } + + public function close() + { + if (!$this->readable) { + return; + } + + $this->readable = false; + $this->stream->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + public function isReadable() + { + return $this->readable; + } + + public function pause() + { + if (!$this->readable) { + return; + } + + $this->stream->pause(); + } + + public function resume() + { + if (!$this->readable) { + return; + } + + $this->stream->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } +} diff --git a/src/Io/Sender.php b/src/Io/Sender.php index e9c0a600..d16b09d0 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -5,8 +5,8 @@ use React\Http\Message\MessageFactory; use Psr\Http\Message\RequestInterface; use React\EventLoop\LoopInterface; -use React\HttpClient\Client as HttpClient; -use React\HttpClient\Response as ResponseStream; +use React\Http\Client\Client as HttpClient; +use React\Http\Client\Response as ResponseStream; use React\Promise\PromiseInterface; use React\Promise\Deferred; use React\Socket\ConnectorInterface; diff --git a/tests/Client/DecodeChunkedStreamTest.php b/tests/Client/DecodeChunkedStreamTest.php new file mode 100644 index 00000000..f238fb6b --- /dev/null +++ b/tests/Client/DecodeChunkedStreamTest.php @@ -0,0 +1,227 @@ + 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' => 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' => 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' => 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' => 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"), + "" + ) + ); + } + + /** + * @test + * @dataProvider provideChunkedEncoding + */ + public function testChunkedEncoding(array $strings, $expected = "Wikipedia in\r\n\r\nchunks.") + { + $stream = new ThroughStream(); + $response = new ChunkedStreamDecoder($stream); + $buffer = ''; + $response->on('data', function ($data) use (&$buffer) { + $buffer .= $data; + }); + $response->on('error', function ($error) { + $this->fail((string)$error); + }); + foreach ($strings as $string) { + $stream->write($string); + } + $this->assertSame($expected, $buffer); + } + + public function provideInvalidChunkedEncoding() + { + 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' => 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") + ) + ); + } + + /** + * @test + * @dataProvider provideInvalidChunkedEncoding + */ + public function testInvalidChunkedEncoding(array $strings) + { + $stream = new ThroughStream(); + $response = new ChunkedStreamDecoder($stream); + $response->on('error', function (Exception $exception) { + throw $exception; + }); + + $this->setExpectedException('Exception'); + foreach ($strings as $string) { + $stream->write($string); + } + } + + public function provideZeroChunk() + { + return array( + array('1-zero' => "0\r\n\r\n"), + array('random-zero' => str_repeat("0", rand(2, 10))."\r\n\r\n") + ); + } + + /** + * @test + * @dataProvider provideZeroChunk + */ + public function testHandleEnd($zeroChunk) + { + $ended = false; + $stream = new ThroughStream(); + $response = new ChunkedStreamDecoder($stream); + $response->on('error', function ($error) { + $this->fail((string)$error); + }); + $response->on('end', function () use (&$ended) { + $ended = true; + }); + + $stream->write("4\r\nWiki\r\n".$zeroChunk); + + $this->assertTrue($ended); + } + + public function testHandleEndIncomplete() + { + $exception = null; + $stream = new ThroughStream(); + $response = new ChunkedStreamDecoder($stream); + $response->on('error', function ($e) use (&$exception) { + $exception = $e; + }); + + $stream->end("4\r\nWiki"); + + $this->assertInstanceOf('Exception', $exception); + } + + public function testHandleEndTrailers() + { + $ended = false; + $stream = new ThroughStream(); + $response = new ChunkedStreamDecoder($stream); + $response->on('error', function ($error) { + $this->fail((string)$error); + }); + $response->on('end', function () use (&$ended) { + $ended = true; + }); + + $stream->write("4\r\nWiki\r\n0\r\nabc: def\r\nghi: klm\r\n\r\n"); + + $this->assertTrue($ended); + } + + /** + * @test + * @dataProvider provideZeroChunk + */ + public function testHandleEndEnsureNoError($zeroChunk) + { + $ended = false; + $stream = new ThroughStream(); + $response = new ChunkedStreamDecoder($stream); + $response->on('error', function ($error) { + $this->fail((string)$error); + }); + $response->on('end', function () use (&$ended) { + $ended = true; + }); + + $stream->write("4\r\nWiki\r\n"); + $stream->write($zeroChunk); + $stream->end(); + + $this->assertTrue($ended); + } +} diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php new file mode 100644 index 00000000..d6cc4b0f --- /dev/null +++ b/tests/Client/FunctionalIntegrationTest.php @@ -0,0 +1,170 @@ +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(); + }); + $port = parse_url(https://github.com/$server-%3EgetAddress(), PHP_URL_PORT); + + $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(); + + Block\await($promise, $loop, self::TIMEOUT_LOCAL); + } + + /** @group internet */ + public function testSuccessfulResponseEmitsEnd() + { + $loop = Factory::create(); + $client = new Client($loop); + + $request = $client->request('GET', 'http://www.google.com/'); + + $once = $this->expectCallableOnce(); + $request->on('response', function (Response $response) use ($once) { + $response->on('end', $once); + }); + + $promise = Stream\first($request, 'close'); + $request->end(); + + 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 */ + public function testCancelPendingConnectionEmitsClose() + { + $loop = Factory::create(); + $client = new Client($loop); + + $request = $client->request('GET', 'http://www.google.com/'); + $request->on('error', $this->expectCallableNever()); + $request->on('close', $this->expectCallableOnce()); + $request->end(); + $request->close(); + } +} diff --git a/tests/Client/RequestDataTest.php b/tests/Client/RequestDataTest.php new file mode 100644 index 00000000..313e140f --- /dev/null +++ b/tests/Client/RequestDataTest.php @@ -0,0 +1,154 @@ +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: React/alpha\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: React/alpha\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: React/alpha\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: React/alpha\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: React/alpha\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: React/alpha\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: React/alpha\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 new file mode 100644 index 00000000..e702d315 --- /dev/null +++ b/tests/Client/RequestTest.php @@ -0,0 +1,714 @@ +stream = $this->getMockBuilder('React\Socket\ConnectionInterface') + ->disableOriginalConstructor() + ->getMock(); + + $this->connector = $this->getMockBuilder('React\Socket\ConnectorInterface') + ->getMock(); + + $this->response = $this->getMockBuilder('React\Http\Client\Response') + ->disableOriginalConstructor() + ->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->at(0)) + ->method('on') + ->with('drain', $this->identicalTo(array($request, 'handleDrain'))); + $this->stream + ->expects($this->at(1)) + ->method('on') + ->with('data', $this->identicalTo(array($request, 'handleData'))); + $this->stream + ->expects($this->at(2)) + ->method('on') + ->with('end', $this->identicalTo(array($request, 'handleEnd'))); + $this->stream + ->expects($this->at(3)) + ->method('on') + ->with('error', $this->identicalTo(array($request, 'handleError'))); + $this->stream + ->expects($this->at(4)) + ->method('on') + ->with('close', $this->identicalTo(array($request, 'handleClose'))); + $this->stream + ->expects($this->at(6)) + ->method('removeListener') + ->with('drain', $this->identicalTo(array($request, 'handleDrain'))); + $this->stream + ->expects($this->at(7)) + ->method('removeListener') + ->with('data', $this->identicalTo(array($request, 'handleData'))); + $this->stream + ->expects($this->at(8)) + ->method('removeListener') + ->with('end', $this->identicalTo(array($request, 'handleEnd'))); + $this->stream + ->expects($this->at(9)) + ->method('removeListener') + ->with('error', $this->identicalTo(array($request, 'handleError'))); + $this->stream + ->expects($this->at(10)) + ->method('removeListener') + ->with('close', $this->identicalTo(array($request, 'handleClose'))); + + $response = $this->response; + + $this->stream->expects($this->once()) + ->method('emit') + ->with('data', $this->identicalTo(array('body'))); + + $response->expects($this->at(0)) + ->method('on') + ->with('close', $this->anything()) + ->will($this->returnCallback(function ($event, $cb) use (&$endCallback) { + $endCallback = $cb; + })); + + $factory = $this->createCallableMock(); + $factory->expects($this->once()) + ->method('__invoke') + ->with('HTTP', '1.0', '200', 'OK', array('Content-Type' => 'text/plain')) + ->will($this->returnValue($response)); + + $request->setResponseFactory($factory); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke') + ->with($response); + + $request->on('response', $handler); + $request->on('end', $this->expectCallableNever()); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke'); + + $request->on('close', $handler); + $request->end(); + + $request->handleData("HTTP/1.0 200 OK\r\n"); + $request->handleData("Content-Type: text/plain\r\n"); + $request->handleData("\r\nbody"); + + $this->assertNotNull($endCallback); + call_user_func($endCallback); + } + + /** @test */ + public function requestShouldEmitErrorIfConnectionFails() + { + $requestData = new RequestData('GET', 'http://www.example.com/'); + $request = new Request($this->connector, $requestData); + + $this->rejectedConnectionMock(); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke') + ->with( + $this->isInstanceOf('RuntimeException') + ); + + $request->on('error', $handler); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke'); + + $request->on('close', $handler); + $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(); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke') + ->with( + $this->isInstanceOf('RuntimeException') + ); + + $request->on('error', $handler); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke'); + + $request->on('close', $handler); + $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(); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke') + ->with( + $this->isInstanceOf('Exception') + ); + + $request->on('error', $handler); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke'); + + $request->on('close', $handler); + $request->on('end', $this->expectCallableNever()); + + $request->end(); + $request->handleError(new \Exception('test')); + } + + /** @test */ + public function requestShouldEmitErrorIfGuzzleParseThrowsException() + { + $requestData = new RequestData('GET', 'http://www.example.com/'); + $request = new Request($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke') + ->with( + $this->isInstanceOf('\InvalidArgumentException') + ); + + $request->on('error', $handler); + + $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); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke') + ->with( + $this->isInstanceOf('\InvalidArgumentException') + ); + + $request->on('error', $handler); + + $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); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke') + ->with( + $this->isInstanceOf('\InvalidArgumentException') + ); + + $request->on('error', $handler); + + $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$#")); + + $factory = $this->createCallableMock(); + $factory->expects($this->once()) + ->method('__invoke') + ->will($this->returnValue($this->response)); + + $request->setResponseFactory($factory); + $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->at(5)) + ->method('write') + ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome$#")); + $this->stream + ->expects($this->at(6)) + ->method('write') + ->with($this->identicalTo("post")); + $this->stream + ->expects($this->at(7)) + ->method('write') + ->with($this->identicalTo("data")); + + $factory = $this->createCallableMock(); + $factory->expects($this->once()) + ->method('__invoke') + ->will($this->returnValue($this->response)); + + $request->setResponseFactory($factory); + + $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->at(5)) + ->method('write') + ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsomepost$#")) + ->willReturn(true); + $this->stream + ->expects($this->at(6)) + ->method('write') + ->with($this->identicalTo("data")); + + $factory = $this->createCallableMock(); + $factory->expects($this->once()) + ->method('__invoke') + ->will($this->returnValue($this->response)); + + $request->setResponseFactory($factory); + + $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->at(0)) + ->method('write') + ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsomepost$#")) + ->willReturn(false); + $this->stream + ->expects($this->at(1)) + ->method('write') + ->with($this->identicalTo("data")); + + $factory = $this->createCallableMock(); + $factory->expects($this->once()) + ->method('__invoke') + ->will($this->returnValue($this->response)); + + $request->setResponseFactory($factory); + + $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->at(5)) + ->method('write') + ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome$#")); + $this->stream + ->expects($this->at(6)) + ->method('write') + ->with($this->identicalTo("post")); + $this->stream + ->expects($this->at(7)) + ->method('write') + ->with($this->identicalTo("data")); + + $factory = $this->createCallableMock(); + $factory->expects($this->once()) + ->method('__invoke') + ->will($this->returnValue($this->response)); + + $loop = $this + ->getMockBuilder('React\EventLoop\LoopInterface') + ->getMock(); + + $request->setResponseFactory($factory); + + $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 requestShouldRelayErrorEventsFromResponse() + { + $requestData = new RequestData('GET', 'http://www.example.com/'); + $request = new Request($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $response = $this->response; + + $response->expects($this->at(0)) + ->method('on') + ->with('close', $this->anything()); + $response->expects($this->at(1)) + ->method('on') + ->with('error', $this->anything()) + ->will($this->returnCallback(function ($event, $cb) use (&$errorCallback) { + $errorCallback = $cb; + })); + + $factory = $this->createCallableMock(); + $factory->expects($this->once()) + ->method('__invoke') + ->with('HTTP', '1.0', '200', 'OK', array('Content-Type' => 'text/plain')) + ->will($this->returnValue($response)); + + $request->setResponseFactory($factory); + $request->end(); + + $request->handleData("HTTP/1.0 200 OK\r\n"); + $request->handleData("Content-Type: text/plain\r\n"); + $request->handleData("\r\nbody"); + + $this->assertNotNull($errorCallback); + call_user_func($errorCallback, new \Exception('test')); + } + + /** @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); + }; + } + + private function rejectedConnectionMock() + { + $this->connector + ->expects($this->once()) + ->method('connect') + ->with('www.example.com:80') + ->will($this->returnValue(new RejectedPromise(new \RuntimeException()))); + } + + /** @test */ + public function multivalueHeader() + { + $requestData = new RequestData('GET', 'http://www.example.com/'); + $request = new Request($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $response = $this->response; + + $response->expects($this->at(0)) + ->method('on') + ->with('close', $this->anything()); + $response->expects($this->at(1)) + ->method('on') + ->with('error', $this->anything()) + ->will($this->returnCallback(function ($event, $cb) use (&$errorCallback) { + $errorCallback = $cb; + })); + + $factory = $this->createCallableMock(); + $factory->expects($this->once()) + ->method('__invoke') + ->with('HTTP', '1.0', '200', 'OK', array('Content-Type' => 'text/plain', 'X-Xss-Protection' => '1; mode=block', 'Cache-Control' => 'public, must-revalidate, max-age=0')) + ->will($this->returnValue($response)); + + $request->setResponseFactory($factory); + $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"); + + $this->assertNotNull($errorCallback); + call_user_func($errorCallback, new \Exception('test')); + } + + /** @test */ + public function chunkedStreamDecoder() + { + $requestData = new RequestData('GET', 'http://www.example.com/'); + $request = new Request($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $request->end(); + + $this->stream->expects($this->once()) + ->method('emit') + ->with('data', array("1\r\nb\r")); + + $request->handleData("HTTP/1.0 200 OK\r\n"); + $request->handleData("Transfer-Encoding: chunked\r\n"); + $request->handleData("\r\n1\r\nb\r"); + $request->handleData("\n3\t\nody\r\n0\t\n\r\n"); + + } +} diff --git a/tests/Client/ResponseTest.php b/tests/Client/ResponseTest.php new file mode 100644 index 00000000..14467239 --- /dev/null +++ b/tests/Client/ResponseTest.php @@ -0,0 +1,168 @@ +stream = $this->getMockBuilder('React\Stream\DuplexStreamInterface') + ->getMock(); + } + + /** @test */ + public function responseShouldEmitEndEventOnEnd() + { + $this->stream + ->expects($this->at(0)) + ->method('on') + ->with('data', $this->anything()); + $this->stream + ->expects($this->at(1)) + ->method('on') + ->with('error', $this->anything()); + $this->stream + ->expects($this->at(2)) + ->method('on') + ->with('end', $this->anything()); + $this->stream + ->expects($this->at(3)) + ->method('on') + ->with('close', $this->anything()); + + $response = new Response($this->stream, 'HTTP', '1.0', '200', 'OK', array('Content-Type' => 'text/plain')); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke') + ->with('some data'); + + $response->on('data', $handler); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke'); + + $response->on('end', $handler); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke'); + + $response->on('close', $handler); + + $this->stream + ->expects($this->at(0)) + ->method('close'); + + $response->handleData('some data'); + $response->handleEnd(); + + $this->assertSame( + array( + 'Content-Type' => 'text/plain' + ), + $response->getHeaders() + ); + } + + /** @test */ + public function closedResponseShouldNotBeResumedOrPaused() + { + $response = new Response($this->stream, 'http', '1.0', '200', 'ok', array('content-type' => 'text/plain')); + + $this->stream + ->expects($this->never()) + ->method('pause'); + $this->stream + ->expects($this->never()) + ->method('resume'); + + $response->handleEnd(); + + $response->resume(); + $response->pause(); + + $this->assertSame( + array( + 'content-type' => 'text/plain', + ), + $response->getHeaders() + ); + } + + /** @test */ + public function chunkedEncodingResponse() + { + $stream = new ThroughStream(); + $response = new Response( + $stream, + 'http', + '1.0', + '200', + 'ok', + array( + 'content-type' => 'text/plain', + 'transfer-encoding' => 'chunked', + ) + ); + + $buffer = ''; + $response->on('data', function ($data) use (&$buffer) { + $buffer.= $data; + }); + $this->assertSame('', $buffer); + $stream->write("4; abc=def\r\n"); + $this->assertSame('', $buffer); + $stream->write("Wiki\r\n"); + $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/Io/SenderTest.php b/tests/Io/SenderTest.php index aaf93ce1..8a04d1f3 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -3,13 +3,13 @@ namespace React\Tests\Http\Io; use Clue\React\Block; +use React\Http\Client\Client as HttpClient; +use React\Http\Client\RequestData; use React\Http\Io\Sender; use React\Http\Message\ReadableBodyStream; -use React\Tests\Http\TestCase; -use React\HttpClient\Client as HttpClient; -use React\HttpClient\RequestData; use React\Promise; use React\Stream\ThroughStream; +use React\Tests\Http\TestCase; use RingCentral\Psr7\Request; class SenderTest extends TestCase @@ -63,13 +63,13 @@ public function testSenderConnectorRejection() public function testSendPostWillAutomaticallySendContentLengthHeader() { - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $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\HttpClient\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -79,13 +79,13 @@ public function testSendPostWillAutomaticallySendContentLengthHeader() public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmptyRequestBody() { - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $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\HttpClient\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -95,10 +95,10 @@ public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmpty public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() { - $outgoing = $this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock(); $outgoing->expects($this->once())->method('write')->with(""); - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->with( 'POST', 'http://www.google.com/', @@ -115,11 +115,11 @@ public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAndRespectRequestThrottling() { - $outgoing = $this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Client\Request')->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); - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -134,12 +134,12 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAn public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() { - $outgoing = $this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Client\Request')->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); - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -153,13 +153,13 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() { - $outgoing = $this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Client\Request')->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'); $outgoing->expects($this->once())->method('close'); - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -183,13 +183,13 @@ public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() { - $outgoing = $this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Client\Request')->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'); $outgoing->expects($this->once())->method('close'); - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -211,13 +211,13 @@ public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() { - $outgoing = $this->getMockBuilder('React\HttpClient\Request')->disableOriginalConstructor()->getMock(); + $outgoing = $this->getMockBuilder('React\Http\Client\Request')->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'); $outgoing->expects($this->never())->method('close'); - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -239,13 +239,13 @@ public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() { - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $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\HttpClient\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -256,13 +256,13 @@ public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() { - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $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\HttpClient\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -272,13 +272,13 @@ public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyRequestBody() { - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $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\HttpClient\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -288,13 +288,13 @@ public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyReques public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsIs() { - $client = $this->getMockBuilder('React\HttpClient\Client')->disableOriginalConstructor()->getMock(); + $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\HttpClient\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); @@ -370,7 +370,7 @@ public function provideRequestProtocolVersion() */ public function testRequestProtocolVersion(Request $Request, $method, $uri, $headers, $protocolVersion) { - $http = $this->getMockBuilder('React\HttpClient\Client') + $http = $this->getMockBuilder('React\Http\Client\Client') ->setMethods(array( 'request', )) @@ -378,7 +378,7 @@ public function testRequestProtocolVersion(Request $Request, $method, $uri, $hea $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(), ))->getMock(); - $request = $this->getMockBuilder('React\HttpClient\Request') + $request = $this->getMockBuilder('React\Http\Client\Request') ->setMethods(array()) ->setConstructorArgs(array( $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(), From 785ded0cce0b8ad6fa641ba9239e9ff43e6d52bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 7 Jul 2020 15:17:00 +0200 Subject: [PATCH 016/152] Organize and rename client and server examples --- README.md | 61 +++++++++++-------- composer.json | 4 +- ...1-google.php => 01-client-get-request.php} | 0 ....php => 02-client-concurrent-requests.php} | 0 .../{03-any.php => 03-client-request-any.php} | 0 ...-post-json.php => 04-client-post-json.php} | 0 .../{05-put-xml.php => 05-client-put-xml.php} | 0 ...y.php => 11-client-http-connect-proxy.php} | 7 ++- ...ks-proxy.php => 12-client-socks-proxy.php} | 4 +- ...-ssh-proxy.php => 13-client-ssh-proxy.php} | 0 ....php => 14-client-unix-domain-sockets.php} | 0 ...21-client-request-streaming-to-stdout.php} | 0 ...=> 22-client-stream-upload-from-stdin.php} | 0 ...lo-world.php => 51-server-hello-world.php} | 0 ...itors.php => 52-server-count-visitors.php} | 0 ...-client-ip.php => 53-server-whatsmyip.php} | 0 ...eter.php => 54-server-query-parameter.php} | 0 ...ling.php => 55-server-cookie-handling.php} | 0 .../{06-sleep.php => 56-server-sleep.php} | 0 ...dling.php => 57-server-error-handling.php} | 0 ...onse.php => 58-server-stream-response.php} | 0 ...09-json-api.php => 59-server-json-api.php} | 2 +- ...ps.php => 61-server-hello-world-https.php} | 0 ...2-upload.php => 62-server-form-upload.php} | 2 +- ...st.php => 63-server-streaming-request.php} | 0 ...ttp-proxy.php => 71-server-http-proxy.php} | 3 + ...y.php => 72-server-http-connect-proxy.php} | 3 + ...de-echo.php => 81-server-upgrade-echo.php} | 0 ...de-chat.php => 82-server-upgrade-chat.php} | 0 ...d.php => 91-client-benchmark-download.php} | 9 ++- ...oad.php => 92-client-benchmark-upload.php} | 9 ++- ...d.php => 99-server-benchmark-download.php} | 7 ++- src/Browser.php | 6 +- src/Server.php | 9 ++- 34 files changed, 77 insertions(+), 49 deletions(-) rename examples/{01-google.php => 01-client-get-request.php} (100%) rename examples/{02-concurrent.php => 02-client-concurrent-requests.php} (100%) rename examples/{03-any.php => 03-client-request-any.php} (100%) rename examples/{04-post-json.php => 04-client-post-json.php} (100%) rename examples/{05-put-xml.php => 05-client-put-xml.php} (100%) rename examples/{11-http-proxy.php => 11-client-http-connect-proxy.php} (82%) rename examples/{12-socks-proxy.php => 12-client-socks-proxy.php} (89%) rename examples/{13-ssh-proxy.php => 13-client-ssh-proxy.php} (100%) rename examples/{14-unix-domain-sockets.php => 14-client-unix-domain-sockets.php} (100%) rename examples/{21-stream-forwarding.php => 21-client-request-streaming-to-stdout.php} (100%) rename examples/{22-stream-stdin.php => 22-client-stream-upload-from-stdin.php} (100%) rename examples/{01-hello-world.php => 51-server-hello-world.php} (100%) rename examples/{02-count-visitors.php => 52-server-count-visitors.php} (100%) rename examples/{03-client-ip.php => 53-server-whatsmyip.php} (100%) rename examples/{04-query-parameter.php => 54-server-query-parameter.php} (100%) rename examples/{05-cookie-handling.php => 55-server-cookie-handling.php} (100%) rename examples/{06-sleep.php => 56-server-sleep.php} (100%) rename examples/{07-error-handling.php => 57-server-error-handling.php} (100%) rename examples/{08-stream-response.php => 58-server-stream-response.php} (100%) rename examples/{09-json-api.php => 59-server-json-api.php} (97%) rename examples/{11-hello-world-https.php => 61-server-hello-world-https.php} (100%) rename examples/{12-upload.php => 62-server-form-upload.php} (98%) rename examples/{13-stream-request.php => 63-server-streaming-request.php} (100%) rename examples/{21-http-proxy.php => 71-server-http-proxy.php} (94%) rename examples/{22-connect-proxy.php => 72-server-http-connect-proxy.php} (93%) rename examples/{31-upgrade-echo.php => 81-server-upgrade-echo.php} (100%) rename examples/{32-upgrade-chat.php => 82-server-upgrade-chat.php} (100%) rename examples/{91-benchmark-download.php => 91-client-benchmark-download.php} (85%) rename examples/{92-benchmark-upload.php => 92-client-benchmark-upload.php} (92%) rename examples/{99-benchmark-download.php => 99-server-benchmark-download.php} (91%) diff --git a/README.md b/README.md index 92877f5a..e4dc0e81 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,18 @@ -# Http +# HTTP [![Build Status](https://travis-ci.org/reactphp/http.svg?branch=master)](https://travis-ci.org/reactphp/http) -Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](https://reactphp.org/). +Event-driven, streaming HTTP client and server implementation for [ReactPHP](https://reactphp.org/). + +This HTTP library provides re-usable implementations for an HTTP client and +server based on ReactPHP's [`Socket`](https://github.com/reactphp/socket) and +[`EventLoop`](https://github.com/reactphp/event-loop) components. +Its client component allows you to send any number of async HTTP/HTTPS requests +concurrently. +Its server component allows you to build plaintext HTTP and secure HTTPS servers +that accept incoming HTTP requests from HTTP clients (such as web browsers). +This library provides async, streaming means for all of this, so you can handle +multiple concurrent HTTP requests without blocking. **Table of contents** @@ -91,8 +101,8 @@ This is an HTTP server which responds with `Hello World!` to every request. ```php $loop = React\EventLoop\Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { - return new Response( +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { + return new React\Http\Response( 200, array( 'Content-Type' => 'text/plain' @@ -107,7 +117,7 @@ $server->listen($socket); $loop->run(); ``` -See also the [examples](examples). +See also the [examples](examples/). ## Client Usage @@ -487,8 +497,8 @@ $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\Respons }); ``` -See also the [stream download example](examples/91-benchmark-download.php) and -the [stream forwarding example](examples/21-stream-forwarding.php). +See also the [stream download benchmark example](examples/91-client-benchmark-download.php) and +the [stream forwarding example](examples/21-client-request-streaming-to-stdout.php). You can invoke the following methods on the message body: @@ -607,7 +617,7 @@ $connector = new React\Socket\Connector($loop, array( $browser = new React\Http\Browser($loop, $connector); ``` -See also the [HTTP CONNECT proxy example](examples/11-http-proxy.php). +See also the [HTTP CONNECT proxy example](examples/11-http-connect-proxy.php). ### SOCKS proxy @@ -738,7 +748,8 @@ $socket = new React\Socket\Server('0.0.0.0:8080', $loop); $server->listen($socket); ``` -See also the [`listen()`](#listen) method and the [first example](../examples/) +See also the [`listen()`](#listen) method and the +[hello world server example](examples/51-server-hello-world.php) for more details. By default, the `Server` buffers and parses the complete incoming HTTP @@ -846,7 +857,8 @@ $socket = new React\Socket\Server('0.0.0.0:8080', $loop); $server->listen($socket); ``` -See also [example #1](examples) for more details. +See also [hello world server example](examples/51-server-hello-world.php) +for more details. This example will start listening for HTTP requests on the alternative HTTP port `8080` on all interfaces (publicly). As an alternative, it is @@ -873,7 +885,8 @@ $socket = new React\Socket\Server('tls://0.0.0.0:8443', $loop, array( $server->listen($socket); ``` -See also [example #11](examples) for more details. +See also [hello world HTTPS example](examples/61-server-hello-world-https.php) +for more details. ### Server Request @@ -945,7 +958,7 @@ $server = new Server(function (ServerRequestInterface $request) { }); ``` -See also [example #3](examples). +See also [whatsmyip server example](examples/53-server-whatsmyip.php). > Advanced: Note that address parameters will not be set if you're listening on a Unix domain socket (UDS) path as this protocol lacks the concept of @@ -983,7 +996,7 @@ Use [`htmlentities`](https://www.php.net/manual/en/function.htmlentities.php) like in this example to prevent [Cross-Site Scripting (abbreviated as XSS)](https://en.wikipedia.org/wiki/Cross-site_scripting). -See also [example #4](examples). +See also [server query parameters example](examples/54-server-query-parameter.php). #### Request body @@ -1022,7 +1035,7 @@ $server = new Server(function (ServerRequestInterface $request) { }); ``` -See also [example #12](examples) for more details. +See also [form upload example](examples/62-server-form-upload.php) for more details. The `getBody(): StreamInterface` method can be used to get the raw data from this request body, similar to @@ -1047,7 +1060,7 @@ $server = new Server(function (ServerRequestInterface $request) { }); ``` -See also [example #9](examples) for more details. +See also [JSON API server example](examples/59-server-json-api.php) for more details. The `getUploadedFiles(): array` method can be used to get the uploaded files in this request, similar to @@ -1070,7 +1083,7 @@ $server = new Server(function (ServerRequestInterface $request) { }); ``` -See also [example #12](examples) for more details. +See also [form upload server example](examples/62-server-form-upload.php) for more details. The `getSize(): ?int` method can be used to get the size of the request body, similar to PHP's `$_SERVER['CONTENT_LENGTH']` variable. @@ -1169,7 +1182,7 @@ $server = new React\Http\Server(array( The above example simply counts the number of bytes received in the request body. This can be used as a skeleton for buffering or processing the request body. -See also [example #13](examples) for more details. +See also [streaming request server example](examples/63-server-streaming-request.php) for more details. The `data` event will be emitted whenever new data is available on the request body stream. @@ -1307,7 +1320,7 @@ non-alphanumeric characters. This encoding is also used internally when decoding the name and value of cookies (which is in line with other implementations, such as PHP's cookie functions). -See also [example #5](examples) for more details. +See also [cookie server example](examples/55-server-cookie-handling.php) for more details. #### Invalid request @@ -1467,7 +1480,7 @@ in this case (if applicable). to look into using [Ratchet](http://socketo.me/) instead. If you want to handle a custom protocol, you will likely want to look into the [HTTP specs](https://tools.ietf.org/html/rfc7230#section-6.7) and also see - [examples #31 and #32](examples) for more details. + [examples #81 and #82](examples/) for more details. In particular, the `101` (Switching Protocols) response code MUST NOT be used unless you send an `Upgrade` response header value that is also present in the corresponding HTTP/1.1 `Upgrade` request header value. @@ -1488,7 +1501,7 @@ in this case (if applicable). requests, one may still be present. Normal request body processing applies here and the connection will only turn to "tunneling mode" after the request body has been processed (which should be empty in most cases). - See also [example #22](examples) for more details. + See also [HTTP CONNECT server example](examples/72-server-http-connect-proxy.php) for more details. #### Response length @@ -1851,7 +1864,7 @@ $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response }); ``` -See also [example 01](examples/01-google.php). +See also [GET request client example](examples/01-client-get-request.php). #### post() @@ -1870,7 +1883,7 @@ $browser->post( }); ``` -See also [example 04](examples/04-post-json.php). +See also [POST JSON client example](examples/04-client-post-json.php). This method is also commonly used to submit HTML form data: @@ -1964,7 +1977,7 @@ $browser->put( }); ``` -See also [example 05](examples/05-put-xml.php). +See also [PUT XML client example](examples/05-client-put-xml.php). This method will automatically add a matching `Content-Length` request header if the outgoing request body is a `string`. If you're using a @@ -2538,7 +2551,7 @@ $server = new Server(array( )); ``` -See also [example #12](examples) for more details. +See also [form upload server example](examples/62-server-form-upload.php) for more details. By default, this middleware respects the [`upload_max_filesize`](https://www.php.net/manual/en/ini.core.php#ini.upload-max-filesize) diff --git a/composer.json b/composer.json index 50afc4db..711d3156 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "react/http", - "description": "Event-driven, streaming plaintext HTTP and secure HTTPS server for ReactPHP", - "keywords": ["event-driven", "streaming", "HTTP", "HTTPS", "server", "ReactPHP"], + "description": "Event-driven, streaming HTTP client and server implementation for ReactPHP", + "keywords": ["HTTP client", "HTTP server", "HTTP", "HTTPS", "event-driven", "streaming", "client", "server", "PSR-7", "async", "ReactPHP"], "license": "MIT", "require": { "php": ">=5.3.0", diff --git a/examples/01-google.php b/examples/01-client-get-request.php similarity index 100% rename from examples/01-google.php rename to examples/01-client-get-request.php diff --git a/examples/02-concurrent.php b/examples/02-client-concurrent-requests.php similarity index 100% rename from examples/02-concurrent.php rename to examples/02-client-concurrent-requests.php diff --git a/examples/03-any.php b/examples/03-client-request-any.php similarity index 100% rename from examples/03-any.php rename to examples/03-client-request-any.php diff --git a/examples/04-post-json.php b/examples/04-client-post-json.php similarity index 100% rename from examples/04-post-json.php rename to examples/04-client-post-json.php diff --git a/examples/05-put-xml.php b/examples/05-client-put-xml.php similarity index 100% rename from examples/05-put-xml.php rename to examples/05-client-put-xml.php diff --git a/examples/11-http-proxy.php b/examples/11-client-http-connect-proxy.php similarity index 82% rename from examples/11-http-proxy.php rename to examples/11-client-http-connect-proxy.php index d1ad9cf5..53d2e91a 100644 --- a/examples/11-http-proxy.php +++ b/examples/11-client-http-connect-proxy.php @@ -1,5 +1,11 @@ /dev/null // $ wget http://localhost:8080/10g.bin -O /dev/null // $ ab -n10 -c10 http://localhost:8080/1g.bin -// $ docker run -it --rm --net=host jordi/ab ab -n10 -c10 http://localhost:8080/1g.bin +// $ docker run -it --rm --net=host jordi/ab -n100000 -c10 http://localhost:8080/ +// $ docker run -it --rm --net=host jordi/ab -n10 -c10 http://localhost:8080/1g.bin use Evenement\EventEmitter; use Psr\Http\Message\ServerRequestInterface; @@ -118,7 +119,7 @@ public function getSize() ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop, array('tcp' => array('backlog' => 511))); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/src/Browser.php b/src/Browser.php index 38479c86..28f90f87 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -73,7 +73,7 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector = * }); * ``` * - * See also [example 01](../examples/01-google.php). + * See also [GET request client example](../examples/01-client-get-request.php). * * @param string $url URL for the request. * @param array $headers @@ -99,7 +99,7 @@ public function get($url, array $headers = array()) * }); * ``` * - * See also [example 04](../examples/04-post-json.php). + * See also [POST JSON client example](../examples/04-client-post-json.php). * * This method is also commonly used to submit HTML form data: * @@ -216,7 +216,7 @@ public function patch($url, array $headers = array(), $contents = '') * }); * ``` * - * See also [example 05](../examples/05-put-xml.php). + * See also [PUT XML client example](../examples/05-client-put-xml.php). * * This method will automatically add a matching `Content-Length` request * header if the outgoing request body is a `string`. If you're using a diff --git a/src/Server.php b/src/Server.php index 3fe942c9..81b6bd0a 100644 --- a/src/Server.php +++ b/src/Server.php @@ -55,7 +55,8 @@ * $server->listen($socket); * ``` * - * See also the [`listen()`](#listen) method and the [first example](../examples/) + * See also the [`listen()`](#listen) method and + * [hello world server example](../examples/51-server-hello-world.php) * for more details. * * By default, the `Server` buffers and parses the complete incoming HTTP @@ -229,7 +230,8 @@ public function __construct($requestHandler) * $server->listen($socket); * ``` * - * See also [example #1](examples) for more details. + * See also [hello world server example](../examples/51-server-hello-world.php) + * for more details. * * This example will start listening for HTTP requests on the alternative * HTTP port `8080` on all interfaces (publicly). As an alternative, it is @@ -256,7 +258,8 @@ public function __construct($requestHandler) * $server->listen($socket); * ``` * - * See also [example #11](examples) for more details. + * See also [hello world HTTPS example](../examples/61-server-hello-world-https.php) + * for more details. * * @param ServerInterface $socket */ From 9be6091857adf6ddf14cca5d312ca8aa64fddb3d Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 7 Jul 2020 18:55:22 +0200 Subject: [PATCH 017/152] Add full core team to the license --- LICENSE | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 0ca9208a..d6f8901f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013 Christian Lück -Copyright (c) 2012 Igor Wiedler, Chris Boden +Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From c0fb86ee81011857ff28c8828b62ef93511f4cf3 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 24 Jun 2020 18:18:42 +0200 Subject: [PATCH 018/152] Add the event loop as required constructor argument For now we won't use it just yet. But this will prepare our API for future features like keep alive connections, dead connection clean up etc. Even though we won't use it in the next release, it will put it in the API so we don't have to break it again when we add features that require the event loop. As a result of this requirement the event loop has been added as a dependency, and the socket and stream packages have been bumped to their stable version. --- README.md | 66 +++---- examples/51-server-hello-world.php | 2 +- examples/52-server-count-visitors.php | 2 +- examples/53-server-whatsmyip.php | 2 +- examples/54-server-query-parameter.php | 2 +- examples/55-server-cookie-handling.php | 2 +- examples/56-server-sleep.php | 2 +- examples/57-server-error-handling.php | 2 +- examples/58-server-stream-response.php | 2 +- examples/59-server-json-api.php | 2 +- examples/61-server-hello-world-https.php | 2 +- examples/62-server-form-upload.php | 2 +- examples/71-server-http-proxy.php | 2 +- examples/72-server-http-connect-proxy.php | 2 +- examples/81-server-upgrade-echo.php | 2 +- examples/82-server-upgrade-chat.php | 2 +- examples/99-server-benchmark-download.php | 2 +- src/Io/StreamingServer.php | 7 +- src/Server.php | 6 +- tests/FunctionalBrowserTest.php | 8 +- tests/FunctionalServerTest.php | 46 ++--- tests/Io/StreamingServerTest.php | 201 +++++++++++----------- tests/ServerTest.php | 18 +- 23 files changed, 196 insertions(+), 188 deletions(-) diff --git a/README.md b/README.md index e4dc0e81..afdaeae1 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ This is an HTTP server which responds with `Hello World!` to every request. ```php $loop = React\EventLoop\Factory::create(); -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Response( 200, array( @@ -714,7 +714,7 @@ the constructor and will be invoked with the respective [request](#server-reques object and expects a [response](#response) object in return: ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Response( 200, array( @@ -742,7 +742,7 @@ chapter. In its most simple form, you can attach this to a to start a plaintext HTTP server like this: ```php -$server = new React\Http\Server($handler); +$server = new React\Http\Server($loop, $handler); $socket = new React\Socket\Server('0.0.0.0:8080', $loop); $server->listen($socket); @@ -803,7 +803,7 @@ to explicitly configure the total number of requests that can be handled at once like this: ```php -$server = new React\Http\Server(array( +$server = new React\Http\Server($loop, array( new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -822,7 +822,7 @@ also use a streaming approach where only small chunks of data have to be kept in memory: ```php -$server = new React\Http\Server(array( +$server = new React\Http\Server($loop, array( new React\Http\Middleware\StreamingRequestMiddleware(), $handler )); @@ -851,7 +851,7 @@ messages. In its most common form, you can attach this to a order to start a plaintext HTTP server like this: ```php -$server = new React\Http\Server($handler); +$server = new React\Http\Server($loop, $handler); $socket = new React\Socket\Server('0.0.0.0:8080', $loop); $server->listen($socket); @@ -877,7 +877,7 @@ using a secure TLS listen address, a certificate file and optional `passphrase` like this: ```php -$server = new React\Http\Server($handler); +$server = new React\Http\Server($loop, $handler); $socket = new React\Socket\Server('tls://0.0.0.0:8443', $loop, array( 'local_cert' => __DIR__ . '/localhost.pem' @@ -902,7 +902,7 @@ which in turn extends the and will be passed to the callback function like this. ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $body = "The method of the request is: " . $request->getMethod(); $body .= "The requested path is: " . $request->getUri()->getPath(); @@ -945,7 +945,7 @@ The following parameters are currently available: Set to 'on' if the request used HTTPS, otherwise it won't be set ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; return new Response( @@ -970,7 +970,7 @@ The `getQueryParams(): array` method can be used to get the query parameters similiar to the `$_GET` variable. ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $queryParams = $request->getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; @@ -1024,7 +1024,7 @@ By default, this method will only return parsed data for requests using request headers (commonly used for `POST` requests for HTML form submission data). ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $name = $request->getParsedBody()['name'] ?? 'anonymous'; return new Response( @@ -1048,7 +1048,7 @@ an XML (`Content-Type: application/xml`) request body (which is commonly used fo `POST`, `PUT` or `PATCH` requests in JSON-based or RESTful/RESTish APIs). ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $data = json_decode((string)$request->getBody()); $name = $data->name ?? 'anonymous'; @@ -1071,7 +1071,7 @@ This array will only be filled when using the `Content-Type: multipart/form-data request header (commonly used for `POST` requests for HTML file uploads). ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $files = $request->getUploadedFiles(); $name = isset($files['avatar']) ? $files['avatar']->getClientFilename() : 'nothing'; @@ -1141,7 +1141,7 @@ The ReactPHP `ReadableStreamInterface` gives you access to the incoming request body as the individual chunks arrive: ```php -$server = new React\Http\Server(array( +$server = new React\Http\Server($loop, array( new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $body = $request->getBody(); @@ -1214,7 +1214,7 @@ This method operates on the streaming request body, i.e. the request body size may be unknown (`null`) when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. ```php -$server = new React\Http\Server(array( +$server = new React\Http\Server($loop, array( new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $size = $request->getBody()->getSize(); @@ -1287,7 +1287,7 @@ The `getCookieParams(): string[]` method can be used to get all cookies sent with the current request. ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $key = 'react\php'; if (isset($request->getCookieParams()[$key])) { @@ -1359,7 +1359,7 @@ but feel free to use any implemantation of the `PSR-7 ResponseInterface` you prefer. ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { return new Response( 200, array( @@ -1382,7 +1382,7 @@ To prevent this you SHOULD use a This example shows how such a long-term action could look like: ```php -$server = new Server(function (ServerRequestInterface $request) use ($loop) { +$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { return new Promise(function ($resolve, $reject) use ($loop) { $loop->addTimer(1.5, function() use ($resolve) { $response = new Response( @@ -1419,7 +1419,7 @@ Note that other implementations of the `PSR-7 ResponseInterface` likely only support strings. ```php -$server = new Server(function (ServerRequestInterface $request) use ($loop) { +$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { $stream = new ThroughStream(); $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { @@ -1510,7 +1510,7 @@ added automatically. This is the most common use case, for example when using a `string` response body like this: ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { return new Response( 200, array( @@ -1529,7 +1529,7 @@ response messages will contain the plain response body. If you know the length of your streaming response body, you MAY want to specify it explicitly like this: ```php -$server = new Server(function (ServerRequestInterface $request) use ($loop) { +$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(2.0, function () use ($stream) { @@ -1608,7 +1608,7 @@ A `Date` header will be automatically added with the system date and time if non You can add a custom `Date` header yourself like this: ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { return new Response( 200, array( @@ -1622,7 +1622,7 @@ If you don't have a appropriate clock to rely on, you should unset this header with an empty string: ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { return new Response( 200, array( @@ -1636,7 +1636,7 @@ Note that it will automatically assume a `X-Powered-By: react/alpha` header unless your specify a custom `X-Powered-By` header yourself: ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { return new Response( 200, array( @@ -1650,7 +1650,7 @@ If you do not want to send this header at all, you can use an empty string as value like this: ```php -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { return new Response( 200, array( @@ -1723,7 +1723,7 @@ The following example adds a middleware request handler that adds the current ti header (`Request-Time`) and a final request handler that always returns a 200 code without a body: ```php -$server = new Server(array( +$server = new Server($loop, array( function (ServerRequestInterface $request, callable $next) { $request = $request->withHeader('Request-Time', time()); return $next($request); @@ -1747,7 +1747,7 @@ In order to simplify handling both paths, you can simply wrap this in a [`Promise\resolve()`](https://reactphp.org/promise/#resolve) call like this: ```php -$server = new Server(array( +$server = new Server($loop, array( function (ServerRequestInterface $request, callable $next) { $promise = React\Promise\resolve($next($request)); return $promise->then(function (ResponseInterface $response) { @@ -1769,7 +1769,7 @@ handling logic (or logging etc.) by wrapping this in a [`Promise`](https://reactphp.org/promise/#promise) like this: ```php -$server = new Server(array( +$server = new Server($loop, array( function (ServerRequestInterface $request, callable $next) { $promise = new React\Promise\Promise(function ($resolve) use ($next, $request) { $resolve($next($request)); @@ -2404,7 +2404,7 @@ The following example shows how this middleware can be used to ensure no more than 10 handlers will be invoked at once: ```php -$server = new Server(array( +$server = new Server($loop, array( new LimitConcurrentRequestsMiddleware(10), $handler )); @@ -2415,7 +2415,7 @@ Similarly, this middleware is often used in combination with the to limit the total number of requests that can be buffered at once: ```php -$server = new Server(array( +$server = new Server($loop, array( new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -2429,7 +2429,7 @@ that can be buffered at once and then ensure the actual request handler only processes one request after another without any concurrency: ```php -$server = new Server(array( +$server = new Server($loop, array( new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -2482,7 +2482,7 @@ the total number of concurrent requests. Usage: ```php -$server = new Server(array( +$server = new Server($loop, array( new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB @@ -2542,7 +2542,7 @@ $handler = function (ServerRequestInterface $request) { ); }; -$server = new Server(array( +$server = new Server($loop, array( new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB diff --git a/examples/51-server-hello-world.php b/examples/51-server-hello-world.php index f703a5d7..c0ad6741 100644 --- a/examples/51-server-hello-world.php +++ b/examples/51-server-hello-world.php @@ -9,7 +9,7 @@ $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { return new Response( 200, array( diff --git a/examples/52-server-count-visitors.php b/examples/52-server-count-visitors.php index 5dec93f9..1fa051f3 100644 --- a/examples/52-server-count-visitors.php +++ b/examples/52-server-count-visitors.php @@ -10,7 +10,7 @@ $loop = Factory::create(); $counter = 0; -$server = new Server(function (ServerRequestInterface $request) use (&$counter) { +$server = new Server($loop, function (ServerRequestInterface $request) use (&$counter) { return new Response( 200, array( diff --git a/examples/53-server-whatsmyip.php b/examples/53-server-whatsmyip.php index 25e3d408..512334eb 100644 --- a/examples/53-server-whatsmyip.php +++ b/examples/53-server-whatsmyip.php @@ -9,7 +9,7 @@ $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; return new Response( diff --git a/examples/54-server-query-parameter.php b/examples/54-server-query-parameter.php index 13015430..aaee50e6 100644 --- a/examples/54-server-query-parameter.php +++ b/examples/54-server-query-parameter.php @@ -9,7 +9,7 @@ $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $queryParams = $request->getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index e09d9277..d96d4a85 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -9,7 +9,7 @@ $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { $key = 'react\php'; if (isset($request->getCookieParams()[$key])) { diff --git a/examples/56-server-sleep.php b/examples/56-server-sleep.php index ae465fb5..141db6ea 100644 --- a/examples/56-server-sleep.php +++ b/examples/56-server-sleep.php @@ -10,7 +10,7 @@ $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) use ($loop) { +$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { return new Promise(function ($resolve, $reject) use ($loop) { $loop->addTimer(1.5, function() use ($resolve) { $response = new Response( diff --git a/examples/57-server-error-handling.php b/examples/57-server-error-handling.php index 76544a6b..625fda17 100644 --- a/examples/57-server-error-handling.php +++ b/examples/57-server-error-handling.php @@ -11,7 +11,7 @@ $loop = Factory::create(); $count = 0; -$server = new Server(function (ServerRequestInterface $request) use (&$count) { +$server = new Server($loop, function (ServerRequestInterface $request) use (&$count) { return new Promise(function ($resolve, $reject) use (&$count) { $count++; diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php index dce54b2b..d246a237 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -10,7 +10,7 @@ $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) use ($loop) { +$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { if ($request->getMethod() !== 'GET' || $request->getUri()->getPath() !== '/') { return new Response(404); } diff --git a/examples/59-server-json-api.php b/examples/59-server-json-api.php index 3702c69c..79f87db4 100644 --- a/examples/59-server-json-api.php +++ b/examples/59-server-json-api.php @@ -15,7 +15,7 @@ $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { if ($request->getHeaderLine('Content-Type') !== 'application/json') { return new Response( 415, // Unsupported Media Type diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index c8bc52e8..19eff50b 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -9,7 +9,7 @@ $loop = Factory::create(); -$server = new Server(function (ServerRequestInterface $request) { +$server = new Server($loop, function (ServerRequestInterface $request) { return new Response( 200, array( diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index 4290fb4c..4db8c5d7 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -124,7 +124,7 @@ // Note how this example explicitly uses the advanced `StreamingRequestMiddleware` to apply // custom request buffering limits below before running our request handler. -$server = new Server(array( +$server = new Server($loop, array( new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers, queue otherwise new RequestBodyBufferMiddleware(8 * 1024 * 1024), // 8 MiB max, ignore body otherwise diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index 57ed8e50..5b829a73 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -17,7 +17,7 @@ // This means that this proxy buffers the whole request before "processing" it. // As such, this is store-and-forward proxy. This could also use the advanced // `StreamingRequestMiddleware` to forward the incoming request as it comes in. -$server = new Server(function (RequestInterface $request) { +$server = new Server($loop, function (RequestInterface $request) { if (strpos($request->getRequestTarget(), '://') === false) { return new Response( 400, diff --git a/examples/72-server-http-connect-proxy.php b/examples/72-server-http-connect-proxy.php index e2fa42a4..6a0364e8 100644 --- a/examples/72-server-http-connect-proxy.php +++ b/examples/72-server-http-connect-proxy.php @@ -19,7 +19,7 @@ // Unlike the plain HTTP proxy, the CONNECT method does not contain a body // and we establish an end-to-end connection over the stream object, so this // doesn't have to store any payload data in memory at all. -$server = new Server(function (ServerRequestInterface $request) use ($connector) { +$server = new Server($loop, function (ServerRequestInterface $request) use ($connector) { if ($request->getMethod() !== 'CONNECT') { return new Response( 405, diff --git a/examples/81-server-upgrade-echo.php b/examples/81-server-upgrade-echo.php index df572d50..6ae2ce8e 100644 --- a/examples/81-server-upgrade-echo.php +++ b/examples/81-server-upgrade-echo.php @@ -30,7 +30,7 @@ // Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // The initial incoming request does not contain a body and we upgrade to a // stream object below. -$server = new Server(function (ServerRequestInterface $request) use ($loop) { +$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { if ($request->getHeaderLine('Upgrade') !== 'echo' || $request->getProtocolVersion() === '1.0') { return new Response( 426, diff --git a/examples/82-server-upgrade-chat.php b/examples/82-server-upgrade-chat.php index 5d60154c..ee4ce146 100644 --- a/examples/82-server-upgrade-chat.php +++ b/examples/82-server-upgrade-chat.php @@ -38,7 +38,7 @@ // Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // The initial incoming request does not contain a body and we upgrade to a // stream object below. -$server = new Server(function (ServerRequestInterface $request) use ($loop, $chat) { +$server = new Server($loop, function (ServerRequestInterface $request) use ($loop, $chat) { if ($request->getHeaderLine('Upgrade') !== 'chat' || $request->getProtocolVersion() === '1.0') { return new Response( 426, diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index 5b5a5abe..bd4acde6 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -87,7 +87,7 @@ public function getSize() } } -$server = new Server(function (ServerRequestInterface $request) use ($loop) { +$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { switch ($request->getUri()->getPath()) { case '/': return new Response( diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 30ab7705..4c20e39a 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -5,6 +5,7 @@ use Evenement\EventEmitter; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use React\EventLoop\LoopInterface; use React\Http\Response; use React\Promise; use React\Promise\CancellablePromiseInterface; @@ -83,6 +84,7 @@ final class StreamingServer extends EventEmitter { private $callback; private $parser; + private $loop; /** * Creates an HTTP server that invokes the given callback for each incoming HTTP request @@ -92,11 +94,14 @@ final class StreamingServer extends EventEmitter * connections in order to then parse incoming data as HTTP. * See also [listen()](#listen) for more details. * + * @param LoopInterface $loop * @param callable|callable[] $requestHandler * @see self::listen() */ - public function __construct($requestHandler) + public function __construct(LoopInterface $loop, $requestHandler) { + $this->loop = $loop; + if (!\is_callable($requestHandler) && !\is_array($requestHandler)) { throw new \InvalidArgumentException('Invalid request handler given'); } elseif (!\is_callable($requestHandler)) { diff --git a/src/Server.php b/src/Server.php index 81b6bd0a..ee1f1f65 100644 --- a/src/Server.php +++ b/src/Server.php @@ -3,6 +3,7 @@ namespace React\Http; use Evenement\EventEmitter; +use React\EventLoop\LoopInterface; use React\Http\Io\IniUtil; use React\Http\Io\StreamingServer; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; @@ -164,10 +165,11 @@ final class Server extends EventEmitter * connections in order to then parse incoming data as HTTP. * See also [listen()](#listen) for more details. * + * @param LoopInterface $loop * @param callable|callable[] $requestHandler * @see self::listen() */ - public function __construct($requestHandler) + public function __construct(LoopInterface $loop, $requestHandler) { if (!\is_callable($requestHandler) && !\is_array($requestHandler)) { throw new \InvalidArgumentException('Invalid request handler given'); @@ -204,7 +206,7 @@ public function __construct($requestHandler) $middleware = \array_merge($middleware, $requestHandler); } - $this->streamingServer = new StreamingServer($middleware); + $this->streamingServer = new StreamingServer($loop, $middleware); $that = $this; $this->streamingServer->on('error', function ($error) use ($that) { diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index c4bbe523..de5a9f9a 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -32,7 +32,7 @@ public function setUpBrowserAndServer() $this->loop = $loop = Factory::create(); $this->browser = new Browser($this->loop); - $server = new Server(array(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use ($loop) { + $server = new Server($this->loop, array(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use ($loop) { $path = $request->getUri()->getPath(); $headers = array(); @@ -527,7 +527,7 @@ public function testPostStreamKnownLength() */ public function testPostStreamWillStartSendingRequestEvenWhenBodyDoesNotEmitData() { - $server = new Server(array(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { + $server = new Server($this->loop, array(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { return new Response(200); })); $socket = new \React\Socket\Server(0, $this->loop); @@ -554,7 +554,7 @@ public function testPostStreamClosed() public function testSendsHttp11ByDefault() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new Server($this->loop, function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -574,7 +574,7 @@ public function testSendsHttp11ByDefault() public function testSendsExplicitHttp10Request() { - $server = new Server(function (ServerRequestInterface $request) { + $server = new Server($this->loop, function (ServerRequestInterface $request) { return new Response( 200, array(), diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index 2de43ce8..f92fb2b0 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -26,7 +26,7 @@ public function testPlainHttpOnRandomPort() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -52,7 +52,7 @@ public function testPlainHttpOnRandomPortWithSingleRequestHandlerArray() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(array( + $server = new Server($loop, array( function () { return new Response(404); }, @@ -79,7 +79,7 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -105,7 +105,7 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -137,7 +137,7 @@ public function testSecureHttpsOnRandomPort() 'tls' => array('verify_peer' => false) )); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -169,7 +169,7 @@ public function testSecureHttpsReturnsData() $loop = Factory::create(); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response( 200, array(), @@ -213,7 +213,7 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() 'tls' => array('verify_peer' => false) )); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -247,7 +247,7 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() } $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -277,7 +277,7 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort } $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -316,7 +316,7 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() 'tls' => array('verify_peer' => false) )); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -355,7 +355,7 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() 'tls' => array('verify_peer' => false) )); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -385,7 +385,7 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() } $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -424,7 +424,7 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() 'tls' => array('verify_peer' => false) )); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri() . 'x' . $request->getHeaderLine('Host')); }); @@ -452,7 +452,7 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() $stream = new ThroughStream(); $stream->close(); - $server = new Server(function (RequestInterface $request) use ($stream) { + $server = new Server($loop, function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); @@ -479,7 +479,7 @@ public function testRequestHandlerWithStreamingRequestWillReceiveCloseEventIfCon $connector = new Connector($loop); $once = $this->expectCallableOnce(); - $server = new Server(array( + $server = new Server($loop, array( new StreamingRequestMiddleware(), function (RequestInterface $request) use ($once) { $request->getBody()->on('close', $once); @@ -509,7 +509,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileS $stream = new ThroughStream(); - $server = new Server(array( + $server = new Server($loop, array( new StreamingRequestMiddleware(), function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); @@ -542,7 +542,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() $stream = new ThroughStream(); - $server = new Server(function (RequestInterface $request) use ($stream) { + $server = new Server($loop, function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); @@ -570,7 +570,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) use ($loop) { + $server = new Server($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -607,7 +607,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) use ($loop) { + $server = new Server($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -645,7 +645,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) use ($loop) { + $server = new Server($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -682,7 +682,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) use ($loop) { + $server = new Server($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -723,7 +723,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(function (RequestInterface $request) { + $server = new Server($loop, function (RequestInterface $request) { $stream = new ThroughStream(); $stream->close(); @@ -757,7 +757,7 @@ public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server(array( + $server = new Server($loop, array( new LimitConcurrentRequestsMiddleware(5), new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (ServerRequestInterface $request, $next) use ($loop) { diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 35396f79..3559c1d2 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -4,6 +4,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Io\StreamingServer; +use React\EventLoop\Factory; use React\Http\Response; use React\Promise\Promise; use React\Stream\ThroughStream; @@ -46,7 +47,7 @@ public function setUpConnectionMockAndSocket() public function testRequestEventWillNotBeEmittedForIncompleteHeaders() { - $server = new StreamingServer($this->expectCallableNever()); + $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -58,7 +59,7 @@ public function testRequestEventWillNotBeEmittedForIncompleteHeaders() public function testRequestEventIsEmitted() { - $server = new StreamingServer($this->expectCallableOnce()); + $server = new StreamingServer(Factory::create(), $this->expectCallableOnce()); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -73,7 +74,7 @@ public function testRequestEventIsEmitted() public function testRequestEventIsEmittedForArrayCallable() { $this->called = null; - $server = new StreamingServer(array($this, 'helperCallableOnce')); + $server = new StreamingServer(Factory::create(), array($this, 'helperCallableOnce')); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -93,7 +94,7 @@ public function testRequestEvent() { $i = 0; $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; }); @@ -126,7 +127,7 @@ public function testRequestEventWithSingleRequestHandlerArray() { $i = 0; $requestAssertion = null; - $server = new StreamingServer(array(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { + $server = new StreamingServer(Factory::create(), array(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; })); @@ -158,7 +159,7 @@ public function testRequestEventWithSingleRequestHandlerArray() public function testRequestGetWithHostAndCustomPort() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -180,7 +181,7 @@ public function testRequestGetWithHostAndCustomPort() public function testRequestGetWithHostAndHttpsPort() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -202,7 +203,7 @@ public function testRequestGetWithHostAndHttpsPort() public function testRequestGetWithHostAndDefaultPortWillBeIgnored() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -224,7 +225,7 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored() public function testRequestOptionsAsterisk() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -244,7 +245,7 @@ public function testRequestOptionsAsterisk() public function testRequestNonOptionsWithAsteriskRequestTargetWillReject() { - $server = new StreamingServer($this->expectCallableNever()); + $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); @@ -257,7 +258,7 @@ public function testRequestNonOptionsWithAsteriskRequestTargetWillReject() public function testRequestConnectAuthorityForm() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -279,7 +280,7 @@ public function testRequestConnectAuthorityForm() public function testRequestConnectWithoutHostWillBeAdded() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -301,7 +302,7 @@ public function testRequestConnectWithoutHostWillBeAdded() public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -323,7 +324,7 @@ public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() public function testRequestConnectAuthorityFormNonMatchingHostWillBeOverwritten() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -344,7 +345,7 @@ public function testRequestConnectAuthorityFormNonMatchingHostWillBeOverwritten( public function testRequestConnectOriginFormRequestTargetWillReject() { - $server = new StreamingServer($this->expectCallableNever()); + $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); @@ -356,7 +357,7 @@ public function testRequestConnectOriginFormRequestTargetWillReject() public function testRequestNonConnectWithAuthorityRequestTargetWillReject() { - $server = new StreamingServer($this->expectCallableNever()); + $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); @@ -370,7 +371,7 @@ public function testRequestWithoutHostEventUsesSocketAddress() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -396,7 +397,7 @@ public function testRequestAbsoluteEvent() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -418,7 +419,7 @@ public function testRequestAbsoluteAddsMissingHostEvent() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -440,7 +441,7 @@ public function testRequestAbsoluteNonMatchingHostWillBeOverwritten() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -462,7 +463,7 @@ public function testRequestOptionsAsteriskEvent() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -484,7 +485,7 @@ public function testRequestOptionsAbsoluteEvent() { $requestAssertion = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -504,7 +505,7 @@ public function testRequestOptionsAbsoluteEvent() public function testRequestPauseWillBeForwardedToConnection() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { $request->getBody()->pause(); }); @@ -524,7 +525,7 @@ public function testRequestPauseWillBeForwardedToConnection() public function testRequestResumeWillBeForwardedToConnection() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { $request->getBody()->resume(); }); @@ -544,7 +545,7 @@ public function testRequestResumeWillBeForwardedToConnection() public function testRequestCloseWillNotCloseConnection() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { $request->getBody()->close(); }); @@ -559,7 +560,7 @@ public function testRequestCloseWillNotCloseConnection() public function testRequestPauseAfterCloseWillNotBeForwarded() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { $request->getBody()->close(); $request->getBody()->pause(); }); @@ -576,7 +577,7 @@ public function testRequestPauseAfterCloseWillNotBeForwarded() public function testRequestResumeAfterCloseWillNotBeForwarded() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { $request->getBody()->close(); $request->getBody()->resume(); }); @@ -595,7 +596,7 @@ public function testRequestEventWithoutBodyWillNotEmitData() { $never = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($never) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($never) { $request->getBody()->on('data', $never); }); @@ -610,7 +611,7 @@ public function testRequestEventWithSecondDataEventWillEmitBodyData() { $once = $this->expectCallableOnceWith('incomplete'); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($once) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($once) { $request->getBody()->on('data', $once); }); @@ -630,7 +631,7 @@ public function testRequestEventWithPartialBodyWillEmitData() { $once = $this->expectCallableOnceWith('incomplete'); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($once) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($once) { $request->getBody()->on('data', $once); }); @@ -651,7 +652,7 @@ public function testRequestEventWithPartialBodyWillEmitData() public function testResponseContainsPoweredByHeader() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response(); }); @@ -681,7 +682,7 @@ public function testResponsePendingPromiseWillNotSendAnything() { $never = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($never) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($never) { return new Promise(function () { }, $never); }); @@ -711,7 +712,7 @@ public function testResponsePendingPromiseWillBeCancelledIfConnectionCloses() { $once = $this->expectCallableOnce(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($once) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($once) { return new Promise(function () { }, $once); }); @@ -743,7 +744,7 @@ public function testRespomseBodyStreamAlreadyClosedWillSendEmptyBodyChunkedEncod $stream = new ThroughStream(); $stream->close(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -778,7 +779,7 @@ public function testResponseBodyStreamEndingWillSendEmptyBodyChunkedEncoded() { $stream = new ThroughStream(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -816,7 +817,7 @@ public function testResponseBodyStreamAlreadyClosedWillSendEmptyBodyPlainHttp10( $stream = new ThroughStream(); $stream->close(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -852,7 +853,7 @@ public function testResponseStreamWillBeClosedIfConnectionIsAlreadyClosed() $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -907,7 +908,7 @@ public function testResponseBodyStreamWillBeClosedIfConnectionEmitsCloseEvent() $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -925,7 +926,7 @@ public function testResponseBodyStreamWillBeClosedIfConnectionEmitsCloseEvent() public function testResponseUpgradeInResponseCanBeUsedToAdvertisePossibleUpgrade() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array( @@ -961,7 +962,7 @@ function ($data) use (&$buffer) { public function testResponseUpgradeWishInRequestCanBeIgnoredByReturningNormalResponse() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array( @@ -996,7 +997,7 @@ function ($data) use (&$buffer) { public function testResponseUpgradeSwitchingProtocolIncludesConnectionUpgradeHeaderWithoutContentLength() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 101, array( @@ -1036,7 +1037,7 @@ public function testResponseUpgradeSwitchingProtocolWithStreamWillPipeDataToConn { $stream = new ThroughStream(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 101, array( @@ -1077,7 +1078,7 @@ public function testResponseConnectMethodStreamWillPipeDataToConnection() { $stream = new ThroughStream(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -1115,7 +1116,7 @@ public function testResponseConnectMethodStreamWillPipeDataFromConnection() { $stream = new ThroughStream(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -1134,7 +1135,7 @@ public function testResponseConnectMethodStreamWillPipeDataFromConnection() public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -1167,7 +1168,7 @@ function ($data) use (&$buffer) { public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp10() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -1201,7 +1202,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyForHeadRequest() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -1233,7 +1234,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContentStatus() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 204, array(), @@ -1266,7 +1267,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyForNotModifiedStatus() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 304, array(), @@ -1300,7 +1301,7 @@ function ($data) use (&$buffer) { public function testRequestInvalidHttpProtocolVersionWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new StreamingServer($this->expectCallableNever()); + $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1334,7 +1335,7 @@ function ($data) use (&$buffer) { public function testRequestOverflowWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new StreamingServer($this->expectCallableNever()); + $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1368,7 +1369,7 @@ function ($data) use (&$buffer) { public function testRequestInvalidWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new StreamingServer($this->expectCallableNever()); + $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1405,7 +1406,7 @@ public function testRequestContentLengthBodyDataWillEmitDataEventOnRequestStream $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1433,7 +1434,7 @@ public function testRequestChunkedTransferEncodingRequestWillEmitDecodedDataEven $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1464,7 +1465,7 @@ public function testRequestChunkedTransferEncodingWithAdditionalDataWontBeEmitte $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1493,7 +1494,7 @@ public function testRequestChunkedTransferEncodingEmpty() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1521,7 +1522,7 @@ public function testRequestChunkedTransferEncodingHeaderCanBeUpperCase() $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1551,7 +1552,7 @@ public function testRequestChunkedTransferEncodingCanBeMixedUpperAndLowerCase() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1578,7 +1579,7 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1609,7 +1610,7 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1641,7 +1642,7 @@ public function testRequestZeroContentLengthWillEmitEndEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1667,7 +1668,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1694,7 +1695,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1720,7 +1721,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(function ($request) use ($errorEvent){ + $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); return \React\Promise\resolve(new Response()); }); @@ -1745,7 +1746,7 @@ public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(function ($request) use ($errorEvent){ + $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -1767,7 +1768,7 @@ public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(function ($request) use ($errorEvent){ + $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -1790,7 +1791,7 @@ public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWi public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(function ($request) use ($errorEvent){ + $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -1812,7 +1813,7 @@ public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() public function testRequestUnexpectedEndOfRequestWithContentLengthWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(function ($request) use ($errorEvent){ + $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -1839,7 +1840,7 @@ public function testRequestWithoutBodyWillEmitEndOnRequestStream() $endEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function ($request) use ($dataEvent, $closeEvent, $endEvent, $errorEvent){ + $server = new StreamingServer(Factory::create(), function ($request) use ($dataEvent, $closeEvent, $endEvent, $errorEvent){ $request->getBody()->on('data', $dataEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('end', $endEvent); @@ -1863,7 +1864,7 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1882,7 +1883,7 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() public function testResponseWithBodyStreamWillUseChunkedTransferEncodingByDefault() { $stream = new ThroughStream(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -1916,7 +1917,7 @@ function ($data) use (&$buffer) { public function testResponseWithBodyStringWillOverwriteExplicitContentLengthAndTransferEncoding() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array( @@ -1957,7 +1958,7 @@ public function testResponseContainsResponseBodyWithTransferEncodingChunkedForBo $body->expects($this->once())->method('getSize')->willReturn(null); $body->expects($this->once())->method('__toString')->willReturn('body'); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($body) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($body) { return new Response( 200, array(), @@ -1994,7 +1995,7 @@ public function testResponseContainsResponseBodyWithPlainBodyWithUnknownSizeForL $body->expects($this->once())->method('getSize')->willReturn(null); $body->expects($this->once())->method('__toString')->willReturn('body'); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($body) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($body) { return new Response( 200, array(), @@ -2028,7 +2029,7 @@ function ($data) use (&$buffer) { public function testResponseWithCustomTransferEncodingWillBeIgnoredAndUseChunkedTransferEncodingInstead() { $stream = new ThroughStream(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array( @@ -2065,7 +2066,7 @@ function ($data) use (&$buffer) { public function testResponseWithoutExplicitDateHeaderWillAddCurrentDate() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response(); }); @@ -2095,7 +2096,7 @@ function ($data) use (&$buffer) { public function testResponseWIthCustomDateHeaderOverwritesDefault() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array("Date" => "Tue, 15 Nov 1994 08:12:31 GMT") @@ -2128,7 +2129,7 @@ function ($data) use (&$buffer) { public function testResponseWithEmptyDateHeaderRemovesDateHeader() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array('Date' => '') @@ -2161,7 +2162,7 @@ function ($data) use (&$buffer) { public function testResponseCanContainMultipleCookieHeaders() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array( @@ -2199,7 +2200,7 @@ function ($data) use (&$buffer) { public function testReponseWithExpectContinueRequestContainsContinueWithLaterResponse() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response(); }); @@ -2231,7 +2232,7 @@ function ($data) use (&$buffer) { public function testResponseWithExpectContinueRequestWontSendContinueForHttp10() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response(); }); @@ -2262,14 +2263,14 @@ function ($data) use (&$buffer) { public function testInvalidCallbackFunctionLeadsToException() { $this->setExpectedException('InvalidArgumentException'); - $server = new StreamingServer('invalid'); + $server = new StreamingServer(Factory::create(), 'invalid'); } public function testResponseBodyStreamWillStreamDataWithChunkedTransferEncoding() { $input = new ThroughStream(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($input) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($input) { return new Response( 200, array(), @@ -2308,7 +2309,7 @@ public function testResponseBodyStreamWithContentLengthWillStreamTillLengthWitho { $input = new ThroughStream(); - $server = new StreamingServer(function (ServerRequestInterface $request) use ($input) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($input) { return new Response( 200, array('Content-Length' => 5), @@ -2346,7 +2347,7 @@ function ($data) use (&$buffer) { public function testResponseWithResponsePromise() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); @@ -2374,7 +2375,7 @@ function ($data) use (&$buffer) { public function testResponseReturnInvalidTypeWillResultInError() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return "invalid"; }); @@ -2408,7 +2409,7 @@ function ($data) use (&$buffer) { public function testResponseResolveWrongTypeInPromiseWillResultInError() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return \React\Promise\resolve("invalid"); }); @@ -2436,7 +2437,7 @@ function ($data) use (&$buffer) { public function testResponseRejectedPromiseWillResultInErrorMessage() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { $reject(new \Exception()); }); @@ -2467,7 +2468,7 @@ function ($data) use (&$buffer) { public function testResponseExceptionInCallbackWillResultInErrorMessage() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { throw new \Exception('Bad call'); }); @@ -2498,7 +2499,7 @@ function ($data) use (&$buffer) { public function testResponseWithContentLengthHeaderForStringBodyOverwritesTransferEncoding() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response( 200, array('Transfer-Encoding' => 'chunked'), @@ -2534,7 +2535,7 @@ function ($data) use (&$buffer) { public function testResponseWillBeHandled() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response(); }); @@ -2562,7 +2563,7 @@ function ($data) use (&$buffer) { public function testResponseExceptionThrowInCallBackFunctionWillResultInErrorMessage() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { throw new \Exception('hello'); }); @@ -2600,7 +2601,7 @@ function ($data) use (&$buffer) { */ public function testResponseThrowableThrowInCallBackFunctionWillResultInErrorMessage() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { throw new \Error('hello'); }); @@ -2643,7 +2644,7 @@ function ($data) use (&$buffer) { public function testResponseRejectOfNonExceptionWillResultInErrorMessage() { - $server = new StreamingServer(function (ServerRequestInterface $request) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { $reject('Invalid type'); }); @@ -2680,7 +2681,7 @@ function ($data) use (&$buffer) { public function testRequestServerRequestParams() { $requestValidation = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2714,7 +2715,7 @@ public function testRequestServerRequestParams() public function testRequestQueryParametersWillBeAddedToRequest() { $requestValidation = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2734,7 +2735,7 @@ public function testRequestQueryParametersWillBeAddedToRequest() public function testRequestCookieWillBeAddedToServerRequest() { $requestValidation = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2755,7 +2756,7 @@ public function testRequestCookieWillBeAddedToServerRequest() public function testRequestInvalidMultipleCookiesWontBeAddedToServerRequest() { $requestValidation = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2776,7 +2777,7 @@ public function testRequestInvalidMultipleCookiesWontBeAddedToServerRequest() public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() { $requestValidation = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2795,7 +2796,7 @@ public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() { $requestValidation = null; - $server = new StreamingServer(function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 02845769..8d8cac5d 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -48,13 +48,13 @@ public function setUpConnectionMockAndSocket() public function testInvalidCallbackFunctionLeadsToException() { $this->setExpectedException('InvalidArgumentException'); - new Server('invalid'); + new Server(Factory::create(), 'invalid'); } public function testSimpleRequestCallsRequestHandlerOnce() { $called = null; - $server = new Server(function (ServerRequestInterface $request) use (&$called) { + $server = new Server(Factory::create(), function (ServerRequestInterface $request) use (&$called) { ++$called; }); @@ -71,7 +71,7 @@ public function testSimpleRequestCallsRequestHandlerOnce() public function testSimpleRequestCallsArrayRequestHandlerOnce() { $this->called = null; - $server = new Server(array($this, 'helperCallableOnce')); + $server = new Server(Factory::create(), array($this, 'helperCallableOnce')); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -88,7 +88,7 @@ public function helperCallableOnce() public function testSimpleRequestWithMiddlewareArrayProcessesMiddlewareStack() { $called = null; - $server = new Server(array( + $server = new Server(Factory::create(), array( function (ServerRequestInterface $request, $next) use (&$called) { $called = 'before'; $ret = $next($request->withHeader('Demo', 'ok')); @@ -112,7 +112,7 @@ public function testPostFileUpload() { $loop = Factory::create(); $deferred = new Deferred(); - $server = new Server(function (ServerRequestInterface $request) use ($deferred) { + $server = new Server($loop, function (ServerRequestInterface $request) use ($deferred) { $deferred->resolve($request); }); @@ -147,7 +147,7 @@ public function testPostFileUpload() public function testServerReceivesBufferedRequestByDefault() { $streaming = null; - $server = new Server(function (ServerRequestInterface $request) use (&$streaming) { + $server = new Server(Factory::create(), function (ServerRequestInterface $request) use (&$streaming) { $streaming = $request->getBody() instanceof ReadableStreamInterface; }); @@ -161,7 +161,7 @@ public function testServerReceivesBufferedRequestByDefault() public function testServerWithStreamingRequestMiddlewareReceivesStreamingRequest() { $streaming = null; - $server = new Server(array( + $server = new Server(Factory::create(), array( new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use (&$streaming) { $streaming = $request->getBody() instanceof ReadableStreamInterface; @@ -179,7 +179,7 @@ public function testForwardErrors() { $exception = new \Exception(); $capturedException = null; - $server = new Server(function () use ($exception) { + $server = new Server(Factory::create(), function () use ($exception) { return Promise\reject($exception); }); $server->on('error', function ($error) use (&$capturedException) { @@ -251,7 +251,7 @@ public function provideIniSettingsForConcurrency() */ public function testServerConcurrency($memory_limit, $post_max_size, $expectedConcurrency) { - $server = new Server(function () { }); + $server = new Server(Factory::create(), function () { }); $ref = new \ReflectionMethod($server, 'getConcurrentRequestsLimit'); $ref->setAccessible(true); From 19b03ff6345998b9e90952834409500a7cbe7575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 8 Jul 2020 20:10:37 +0200 Subject: [PATCH 019/152] Move `Response` class to `React\Http\Message\Response` --- README.md | 76 +++++++++++++------ examples/51-server-hello-world.php | 2 +- examples/52-server-count-visitors.php | 2 +- examples/53-server-whatsmyip.php | 2 +- examples/54-server-query-parameter.php | 2 +- examples/55-server-cookie-handling.php | 2 +- examples/56-server-sleep.php | 2 +- examples/57-server-error-handling.php | 2 +- examples/58-server-stream-response.php | 2 +- examples/59-server-json-api.php | 2 +- examples/61-server-hello-world-https.php | 2 +- examples/62-server-form-upload.php | 2 +- examples/63-server-streaming-request.php | 4 +- examples/71-server-http-proxy.php | 2 +- examples/72-server-http-connect-proxy.php | 2 +- examples/81-server-upgrade-echo.php | 2 +- examples/82-server-upgrade-chat.php | 2 +- examples/99-server-benchmark-download.php | 2 +- src/Io/StreamingServer.php | 5 +- src/Message/Response.php | 56 ++++++++++++++ src/Response.php | 36 --------- src/Server.php | 6 +- tests/FunctionalBrowserTest.php | 2 +- tests/FunctionalServerTest.php | 2 +- tests/Io/StreamingServerTest.php | 2 +- tests/{ => Message}/ResponseTest.php | 5 +- .../LimitConcurrentRequestsMiddlewareTest.php | 2 +- .../RequestBodyBufferMiddlewareTest.php | 2 +- .../StreamingRequestMiddlewareTest.php | 2 +- tests/benchmark-middleware-runner.php | 2 +- 30 files changed, 143 insertions(+), 91 deletions(-) create mode 100644 src/Message/Response.php delete mode 100644 src/Response.php rename tests/{ => Message}/ResponseTest.php (83%) diff --git a/README.md b/README.md index afdaeae1..73539d7a 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ multiple concurrent HTTP requests without blocking. * [Request method](#request-method) * [Cookie parameters](#cookie-parameters) * [Invalid request](#invalid-request) - * [Response](#response) + * [Server Response](#server-response) * [Deferred response](#deferred-response) * [Streaming outgoing response](#streaming-outgoing-response) * [Response length](#response-length) @@ -68,6 +68,8 @@ multiple concurrent HTTP requests without blocking. * [withBase()](#withbase) * [withProtocolVersion()](#withprotocolversion) * [withResponseBuffer()](#withresponsebuffer) + * [React\Http\Message](#reacthttpmessage) + * [Response](#response) * [React\Http\Middleware](#reacthttpmiddleware) * [StreamingRequestMiddleware](#streamingrequestmiddleware) * [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) @@ -102,7 +104,7 @@ This is an HTTP server which responds with `Hello World!` to every request. $loop = React\EventLoop\Factory::create(); $server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -711,11 +713,11 @@ processing each incoming HTTP request. When a complete HTTP request has been received, it will invoke the given request handler function. This request handler function needs to be passed to the constructor and will be invoked with the respective [request](#server-request) -object and expects a [response](#response) object in return: +object and expects a [response](#server-response) object in return: ```php $server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -731,7 +733,7 @@ see also following [request](#server-request) chapter for more details. Each outgoing HTTP response message is always represented by the [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), -see also following [response](#response) chapter for more details. +see also following [response](#server-response) chapter for more details. In order to start listening for any incoming connections, the `Server` needs to be attached to an instance of @@ -1155,7 +1157,7 @@ $server = new React\Http\Server($loop, array( }); $body->on('end', function () use ($resolve, &$bytes){ - $resolve(new React\Http\Response( + $resolve(new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -1166,7 +1168,7 @@ $server = new React\Http\Server($loop, array( // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event $body->on('error', function (\Exception $exception) use ($resolve, &$bytes) { - $resolve(new React\Http\Response( + $resolve(new React\Http\Message\Response( 400, array( 'Content-Type' => 'text/plain' @@ -1222,7 +1224,7 @@ $server = new React\Http\Server($loop, array( $body = 'The request does not contain an explicit length.'; $body .= 'This example does not accept chunked transfer encoding.'; - return new React\Http\Response( + return new React\Http\Message\Response( 411, array( 'Content-Type' => 'text/plain' @@ -1231,7 +1233,7 @@ $server = new React\Http\Server($loop, array( ); } - return new React\Http\Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -1342,25 +1344,24 @@ Note that the server will also emit an `error` event if you do not return a valid response object from your request handler function. See also [invalid response](#invalid-response) for more details. -### Response +### Server Response The callback function passed to the constructor of the [`Server`](#server) is responsible for processing the request and returning a response, which will be -delivered to the client. This function MUST return an instance implementing -[PSR-7 ResponseInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#33-psrhttpmessageresponseinterface) +delivered to the client. + +This function MUST return an instance implementing +[PSR-7 `ResponseInterface`](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#33-psrhttpmessageresponseinterface) object or a -[ReactPHP Promise](https://github.com/reactphp/promise#reactpromise) -which will resolve a `PSR-7 ResponseInterface` object. +[ReactPHP Promise](https://github.com/reactphp/promise) +which resolves with a PSR-7 `ResponseInterface` object. -You will find a `Response` class -which implements the `PSR-7 ResponseInterface` in this project. -We use instantiation of this class in our projects, -but feel free to use any implemantation of the -`PSR-7 ResponseInterface` you prefer. +This projects ships a [`Response` class](#response) which implements the PSR-7 +`ResponseInterface`. In its most simple form, you can use it like this: ```php -$server = new Server($loop, function (ServerRequestInterface $request) { - return new Response( +$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -1370,6 +1371,10 @@ $server = new Server($loop, function (ServerRequestInterface $request) { }); ``` +We use this [`Response` class](#response) throughout our project examples, but +feel free to use any other implementation of the PSR-7 `ResponseInterface`. +See also the [`Response` class](#response) for more details. + #### Deferred response The example above returns the response directly, because it needs @@ -2324,6 +2329,33 @@ 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. +### React\Http\Message + +#### Response + +The `Response` class can be used to +represent an outgoing server response message. + +```php +$response = new React\Http\Message\Response( + 200, + array( + 'Content-Type' => 'text/html' + ), + "Hello world!\n" +); +``` + +This class implements the +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) +which in turn extends the +[PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). + +> Internally, this class extends the underlying `\RingCentral\Psr7\Response` + class. The only difference is that this class will accept implemenations + of ReactPHPs `ReadableStreamInterface` for the `$body` argument. This base + class is considered an implementation detail that may change in the future. + ### React\Http\Middleware #### StreamingRequestMiddleware @@ -2350,7 +2382,7 @@ $server = new React\Http\Server(array( $bytes += \count($chunk); }); $body->on('close', function () use (&$bytes, $resolve) { - $resolve(new React\Http\Response( + $resolve(new React\Http\Message\Response( 200, [], "Received $bytes bytes\n" diff --git a/examples/51-server-hello-world.php b/examples/51-server-hello-world.php index c0ad6741..f6903cff 100644 --- a/examples/51-server-hello-world.php +++ b/examples/51-server-hello-world.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; diff --git a/examples/52-server-count-visitors.php b/examples/52-server-count-visitors.php index 1fa051f3..2b8e897c 100644 --- a/examples/52-server-count-visitors.php +++ b/examples/52-server-count-visitors.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; diff --git a/examples/53-server-whatsmyip.php b/examples/53-server-whatsmyip.php index 512334eb..18f7504e 100644 --- a/examples/53-server-whatsmyip.php +++ b/examples/53-server-whatsmyip.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; diff --git a/examples/54-server-query-parameter.php b/examples/54-server-query-parameter.php index aaee50e6..2786f380 100644 --- a/examples/54-server-query-parameter.php +++ b/examples/54-server-query-parameter.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index d96d4a85..6faf6be7 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; diff --git a/examples/56-server-sleep.php b/examples/56-server-sleep.php index 141db6ea..3da6963b 100644 --- a/examples/56-server-sleep.php +++ b/examples/56-server-sleep.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use React\Promise\Promise; diff --git a/examples/57-server-error-handling.php b/examples/57-server-error-handling.php index 625fda17..c8e99ee4 100644 --- a/examples/57-server-error-handling.php +++ b/examples/57-server-error-handling.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use React\Promise\Promise; diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php index d246a237..518c2cb4 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use React\Stream\ThroughStream; diff --git a/examples/59-server-json-api.php b/examples/59-server-json-api.php index 79f87db4..8602a889 100644 --- a/examples/59-server-json-api.php +++ b/examples/59-server-json-api.php @@ -8,7 +8,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index 19eff50b..dfe3e941 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -2,7 +2,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index 4db8c5d7..c997a465 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -10,11 +10,11 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\UploadedFileInterface; use React\EventLoop\Factory; +use React\Http\Message\Response; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\RequestBodyParserMiddleware; use React\Http\Middleware\StreamingRequestMiddleware; -use React\Http\Response; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index 6b9c8cec..9c1a9758 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -23,7 +23,7 @@ function (Psr\Http\Message\ServerRequestInterface $request) { }); $body->on('end', function () use ($resolve, &$bytes){ - $resolve(new React\Http\Response( + $resolve(new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -34,7 +34,7 @@ function (Psr\Http\Message\ServerRequestInterface $request) { // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event $body->on('error', function (\Exception $exception) use ($resolve, &$bytes) { - $resolve(new React\Http\Response( + $resolve(new React\Http\Message\Response( 400, array( 'Content-Type' => 'text/plain' diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index 5b829a73..b959b7bf 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -5,7 +5,7 @@ use Psr\Http\Message\RequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use RingCentral\Psr7; diff --git a/examples/72-server-http-connect-proxy.php b/examples/72-server-http-connect-proxy.php index 6a0364e8..e786da76 100644 --- a/examples/72-server-http-connect-proxy.php +++ b/examples/72-server-http-connect-proxy.php @@ -5,7 +5,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use React\Socket\Connector; use React\Socket\ConnectionInterface; diff --git a/examples/81-server-upgrade-echo.php b/examples/81-server-upgrade-echo.php index 6ae2ce8e..34e85f6c 100644 --- a/examples/81-server-upgrade-echo.php +++ b/examples/81-server-upgrade-echo.php @@ -19,7 +19,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use React\Stream\ThroughStream; diff --git a/examples/82-server-upgrade-chat.php b/examples/82-server-upgrade-chat.php index ee4ce146..5e49ce37 100644 --- a/examples/82-server-upgrade-chat.php +++ b/examples/82-server-upgrade-chat.php @@ -21,7 +21,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use React\Stream\CompositeStream; use React\Stream\ThroughStream; diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index bd4acde6..536f4515 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -10,7 +10,7 @@ use Evenement\EventEmitter; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use React\Stream\ReadableStreamInterface; use React\Stream\WritableStreamInterface; diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 4c20e39a..e2044d2c 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -6,7 +6,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; -use React\Http\Response; +use React\Http\Message\Response; use React\Promise; use React\Promise\CancellablePromiseInterface; use React\Promise\PromiseInterface; @@ -75,8 +75,7 @@ * [streaming request](#streaming-request) below for more details. * * @see \React\Http\Server - * @see Request - * @see Response + * @see \React\Http\Message\Response * @see self::listen() * @internal */ diff --git a/src/Message/Response.php b/src/Message/Response.php new file mode 100644 index 00000000..a4aabe38 --- /dev/null +++ b/src/Message/Response.php @@ -0,0 +1,56 @@ + 'text/html' + * ), + * "Hello world!\n" + * ); + * ``` + * + * This class implements the + * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) + * which in turn extends the + * [PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). + * + * > Internally, this class extends the underlying `\RingCentral\Psr7\Response` + * class. The only difference is that this class will accept implemenations + * of ReactPHPs `ReadableStreamInterface` for the `$body` argument. This base + * class is considered an implementation detail that may change in the future. + * + * @see \Psr\Http\Message\ResponseInterface + */ +class Response extends Psr7Response +{ + public function __construct( + $status = 200, + array $headers = array(), + $body = null, + $version = '1.1', + $reason = null + ) { + if ($body instanceof ReadableStreamInterface) { + $body = new HttpBodyStream($body, null); + } + + parent::__construct( + $status, + $headers, + $body, + $version, + $reason + ); + } +} diff --git a/src/Response.php b/src/Response.php deleted file mode 100644 index 0964dac9..00000000 --- a/src/Response.php +++ /dev/null @@ -1,36 +0,0 @@ - 'text/plain' @@ -39,7 +39,7 @@ * * Each outgoing HTTP response message is always represented by the * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), - * see also following [response](#response) chapter for more details. + * see also following [response](#server-response) chapter for more details. * * In order to start listening for any incoming connections, the `Server` needs * to be attached to an instance of diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index de5a9f9a..6225b2f1 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -9,7 +9,7 @@ use React\Http\Browser; use React\Http\Message\ResponseException; use React\Http\Middleware\StreamingRequestMiddleware; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use React\Promise\Promise; use React\Promise\Stream; diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index f92fb2b0..a57a5dce 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -5,7 +5,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; -use React\Http\Response; +use React\Http\Message\Response; use React\Http\Server; use React\Socket\Server as Socket; use React\EventLoop\Factory; diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 3559c1d2..6df6cf79 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -5,7 +5,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Io\StreamingServer; use React\EventLoop\Factory; -use React\Http\Response; +use React\Http\Message\Response; use React\Promise\Promise; use React\Stream\ThroughStream; use React\Tests\Http\SocketServerStub; diff --git a/tests/ResponseTest.php b/tests/Message/ResponseTest.php similarity index 83% rename from tests/ResponseTest.php rename to tests/Message/ResponseTest.php index 146278f1..13646bd8 100644 --- a/tests/ResponseTest.php +++ b/tests/Message/ResponseTest.php @@ -1,9 +1,10 @@ Date: Wed, 8 Jul 2020 22:16:10 +0200 Subject: [PATCH 020/152] Validate `Response` body type --- src/Message/Response.php | 14 ++++++++++-- tests/Message/ResponseTest.php | 40 +++++++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/Message/Response.php b/src/Message/Response.php index a4aabe38..45d007ee 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -34,15 +34,25 @@ */ class Response extends Psr7Response { + /** + * @param int $status HTTP status code (e.g. 200/404) + * @param array $headers additional response headers + * @param string|ReadableStreamInterface|StreamInterface $body response body + * @param string $version HTTP protocol version (e.g. 1.1/1.0) + * @param ?string $reason custom HTTP response phrase + * @throws \InvalidArgumentException for an invalid body + */ public function __construct( $status = 200, array $headers = array(), - $body = null, + $body = '', $version = '1.1', $reason = null ) { - if ($body instanceof ReadableStreamInterface) { + if ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) { $body = new HttpBodyStream($body, null); + } elseif (!\is_string($body) && !$body instanceof StreamInterface) { + throw new \InvalidArgumentException('Invalid response body given'); } parent::__construct( diff --git a/tests/Message/ResponseTest.php b/tests/Message/ResponseTest.php index 13646bd8..457618e9 100644 --- a/tests/Message/ResponseTest.php +++ b/tests/Message/ResponseTest.php @@ -2,21 +2,51 @@ namespace React\Tests\Http\Message; +use React\Http\Io\HttpBodyStream; use React\Http\Message\Response; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; class ResponseTest extends TestCase { - public function testResponseBodyWillBeHttpBodyStream() - { - $response = new Response(200, array(), new ThroughStream()); - $this->assertInstanceOf('React\Http\Io\HttpBodyStream', $response->getBody()); - } public function testStringBodyWillBePsr7Stream() { $response = new Response(200, array(), 'hello'); $this->assertInstanceOf('RingCentral\Psr7\Stream', $response->getBody()); } + + public function testConstructWithStreamingBodyWillReturnReadableBodyStream() + { + $response = new Response(200, array(), new ThroughStream()); + + $body = $response->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceof('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf('React\Http\Io\HttpBodyStream', $body); + $this->assertNull($body->getSize()); + } + + public function testConstructWithHttpBodyStreamReturnsBodyAsIs() + { + $response = new Response( + 200, + array(), + $body = new HttpBodyStream(new ThroughStream(), 100) + ); + + $this->assertSame($body, $response->getBody()); + } + + public function testFloatBodyWillThrow() + { + $this->setExpectedException('InvalidArgumentException'); + new Response(200, array(), 1.0); + } + + public function testResourceBodyWillThrow() + { + $this->setExpectedException('InvalidArgumentException'); + new Response(200, array(), tmpfile()); + } } From 76fb422e841c372a97262c04549e717ca385ad67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 8 Jul 2020 22:28:21 +0200 Subject: [PATCH 021/152] Move `ServerRequest` class to `React\Http\Message\ServerRequest` --- README.md | 21 ++++++++++++++++++ src/Io/RequestHeaderParser.php | 1 + src/Io/StreamingServer.php | 1 + src/{Io => Message}/ServerRequest.php | 22 ++++++++++++------- tests/Io/MiddlewareRunnerTest.php | 8 +++---- tests/Io/MultipartParserTest.php | 2 +- tests/Io/StreamingServerTest.php | 2 +- tests/{Io => Message}/ServerRequestTest.php | 4 ++-- .../LimitConcurrentRequestsMiddlewareTest.php | 6 ++--- .../RequestBodyBufferMiddlewareTest.php | 4 ++-- .../RequestBodyParserMiddlewareTest.php | 2 +- .../StreamingRequestMiddlewareTest.php | 2 +- tests/benchmark-middleware-runner.php | 4 ++-- 13 files changed, 54 insertions(+), 25 deletions(-) rename src/{Io => Message}/ServerRequest.php (81%) rename tests/{Io => Message}/ServerRequestTest.php (99%) diff --git a/README.md b/README.md index 73539d7a..452b94b4 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ multiple concurrent HTTP requests without blocking. * [withResponseBuffer()](#withresponsebuffer) * [React\Http\Message](#reacthttpmessage) * [Response](#response) + * [ServerRequest](#serverrequest) * [React\Http\Middleware](#reacthttpmiddleware) * [StreamingRequestMiddleware](#streamingrequestmiddleware) * [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) @@ -2356,6 +2357,26 @@ which in turn extends the of ReactPHPs `ReadableStreamInterface` for the `$body` argument. This base class is considered an implementation detail that may change in the future. +#### ServerRequest + +The `ServerRequest` class can be used to +respresent an incoming server request message. + +This class implements the +[PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface) +which extends the +[PSR-7 `RequestInterface`](https://www.php-fig.org/psr/psr-7/#32-psrhttpmessagerequestinterface) +which in turn extends the +[PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). + +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 + considered an implementation detail that may change in the future. + ### React\Http\Middleware #### StreamingRequestMiddleware diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index f7f77e7e..d26d6548 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -4,6 +4,7 @@ use Evenement\EventEmitter; use Psr\Http\Message\ServerRequestInterface; +use React\Http\Message\ServerRequest; use React\Socket\ConnectionInterface; use Exception; diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index e2044d2c..8b104597 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -7,6 +7,7 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; use React\Http\Message\Response; +use React\Http\Message\ServerRequest; use React\Promise; use React\Promise\CancellablePromiseInterface; use React\Promise\PromiseInterface; diff --git a/src/Io/ServerRequest.php b/src/Message/ServerRequest.php similarity index 81% rename from src/Io/ServerRequest.php rename to src/Message/ServerRequest.php index 28a8c5db..71d10358 100644 --- a/src/Io/ServerRequest.php +++ b/src/Message/ServerRequest.php @@ -1,6 +1,6 @@ Internally, this implementation builds on top of an existing outgoing + * request message and only adds required server methods. This base class is + * considered an implementation detail that may change in the future. * * @see ServerRequestInterface - * @internal */ class ServerRequest extends Request implements ServerRequestInterface { diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index f43231b0..eda61012 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -3,18 +3,18 @@ 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\EventLoop\Factory; use React\Http\Io\MiddlewareRunner; -use React\Http\Io\ServerRequest; +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; -use Psr\Http\Message\RequestInterface; -use React\Promise\CancellablePromiseInterface; -use React\Promise\PromiseInterface; final class MiddlewareRunnerTest extends TestCase { diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 91ec832d..14550f57 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -3,7 +3,7 @@ namespace React\Tests\Http\Io\Middleware; use React\Http\Io\MultipartParser; -use React\Http\Io\ServerRequest; +use React\Http\Message\ServerRequest; use React\Tests\Http\TestCase; final class MultipartParserTest extends TestCase diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 6df6cf79..e4e4a1f3 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -3,8 +3,8 @@ namespace React\Tests\Http\Io; use Psr\Http\Message\ServerRequestInterface; -use React\Http\Io\StreamingServer; use React\EventLoop\Factory; +use React\Http\Io\StreamingServer; use React\Http\Message\Response; use React\Promise\Promise; use React\Stream\ThroughStream; diff --git a/tests/Io/ServerRequestTest.php b/tests/Message/ServerRequestTest.php similarity index 99% rename from tests/Io/ServerRequestTest.php rename to tests/Message/ServerRequestTest.php index 47346cd1..4785486d 100644 --- a/tests/Io/ServerRequestTest.php +++ b/tests/Message/ServerRequestTest.php @@ -1,8 +1,8 @@ Date: Thu, 9 Jul 2020 11:11:55 +0200 Subject: [PATCH 022/152] Validate `ServerRequest` body type --- src/Io/RequestHeaderParser.php | 2 +- src/Message/ServerRequest.php | 45 ++++++++----- tests/Message/ServerRequestTest.php | 99 +++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 17 deletions(-) diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index d26d6548..53f7ff09 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -224,7 +224,7 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) $start['method'], $uri, $fields, - null, + '', $start['version'], $serverParams ); diff --git a/src/Message/ServerRequest.php b/src/Message/ServerRequest.php index 71d10358..b0d64498 100644 --- a/src/Message/ServerRequest.php +++ b/src/Message/ServerRequest.php @@ -5,6 +5,8 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; +use React\Http\Io\HttpBodyStream; +use React\Stream\ReadableStreamInterface; use RingCentral\Psr7\Request; /** @@ -38,25 +40,40 @@ class ServerRequest extends Request implements ServerRequestInterface private $parsedBody; /** - * @param null|string $method HTTP method for the request. - * @param null|string|UriInterface $uri URI for the request. - * @param array $headers Headers for the message. - * @param string|resource|StreamInterface $body Message body. - * @param string $protocolVersion HTTP protocol version. - * @param array $serverParams server-side parameters - * - * @throws \InvalidArgumentException for an invalid URI + * @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. + * @param array $serverParams server-side parameters + * @throws \InvalidArgumentException for an invalid URL or body */ public function __construct( $method, - $uri, + $url, array $headers = array(), - $body = null, - $protocolVersion = '1.1', + $body = '', + $version = '1.1', $serverParams = array() ) { + $stream = null; + if ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) { + $stream = $body; + $body = null; + } elseif (!\is_string($body) && !$body instanceof StreamInterface) { + throw new \InvalidArgumentException('Invalid server request body given'); + } + $this->serverParams = $serverParams; - parent::__construct($method, $uri, $headers, $body, $protocolVersion); + 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); + } $query = $this->getUri()->getQuery(); if ($query !== '') { @@ -158,10 +175,6 @@ public function withoutAttribute($name) */ private function parseCookie($cookie) { - if ($cookie === '') { - return array(); - } - $cookieArray = \explode(';', $cookie); $result = array(); diff --git a/tests/Message/ServerRequestTest.php b/tests/Message/ServerRequestTest.php index 4785486d..37cc1879 100644 --- a/tests/Message/ServerRequestTest.php +++ b/tests/Message/ServerRequestTest.php @@ -2,7 +2,9 @@ namespace React\Tests\Http\Message; +use React\Http\Io\HttpBodyStream; use React\Http\Message\ServerRequest; +use React\Stream\ThroughStream; use React\Tests\Http\TestCase; class ServerRequestTest extends TestCase @@ -263,4 +265,101 @@ public function testCookieWithoutSpaceAfterSeparatorWillBeAccepted() $cookies = $this->request->getCookieParams(); $this->assertEquals(array('hello' => 'world', 'react' => 'php'), $cookies); } + + public function testConstructWithStringRequestBodyReturnsStringBodyWithAutomaticSize() + { + $request = new ServerRequest( + 'GET', + 'http://localhost/', + array(), + 'foo' + ); + + $body = $request->getBody(); + $this->assertSame(3, $body->getSize()); + $this->assertEquals('foo', (string) $body); + } + + public function testConstructWithHttpBodyStreamReturnsBodyAsIs() + { + $request = new ServerRequest( + 'GET', + 'http://localhost/', + array(), + $body = new HttpBodyStream(new ThroughStream(), 100) + ); + + $this->assertSame($body, $request->getBody()); + } + + public function testConstructWithStreamingRequestBodyReturnsBodyWhichImplementsReadableStreamInterfaceWithSizeZeroDefault() + { + $request = new ServerRequest( + 'GET', + 'http://localhost/', + array(), + new ThroughStream() + ); + + $body = $request->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertSame(0, $body->getSize()); + } + + public function testConstructWithStreamingRequestBodyReturnsBodyWithSizeFromContentLengthHeader() + { + $request = new ServerRequest( + 'GET', + 'http://localhost/', + array( + 'Content-Length' => 100 + ), + new ThroughStream() + ); + + $body = $request->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertSame(100, $body->getSize()); + } + + public function testConstructWithStreamingRequestBodyReturnsBodyWithSizeUnknownForTransferEncodingChunked() + { + $request = new ServerRequest( + 'GET', + 'http://localhost/', + array( + 'Transfer-Encoding' => 'Chunked' + ), + new ThroughStream() + ); + + $body = $request->getBody(); + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertNull($body->getSize()); + } + + public function testConstructWithFloatRequestBodyThrows() + { + $this->setExpectedException('InvalidArgumentException'); + new ServerRequest( + 'GET', + 'http://localhost/', + array(), + 1.0 + ); + } + + public function testConstructWithResourceRequestBodyThrows() + { + $this->setExpectedException('InvalidArgumentException'); + new ServerRequest( + 'GET', + 'http://localhost/', + array(), + tmpfile() + ); + } } From 1666348682f82c169b6215b67b84995db157267e Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 24 Jun 2020 18:48:59 +0200 Subject: [PATCH 023/152] Drop array request handler support Instead use func_get_args() and turn any callable added after the event loop as constructor argument into a middleware and use the last one as request handler. --- README.md | 60 ++++++++++++++++++------------ examples/62-server-form-upload.php | 5 ++- src/Io/StreamingServer.php | 10 ++--- src/Server.php | 20 +++++----- tests/FunctionalBrowserTest.php | 8 ++-- tests/FunctionalServerTest.php | 22 ++++++----- tests/Io/StreamingServerTest.php | 4 +- tests/ServerTest.php | 10 +++-- 8 files changed, 78 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index afdaeae1..891c9f7e 100644 --- a/README.md +++ b/README.md @@ -803,13 +803,14 @@ to explicitly configure the total number of requests that can be handled at once like this: ```php -$server = new React\Http\Server($loop, array( +$server = new React\Http\Server( + $loop, new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request new React\Http\Middleware\RequestBodyParserMiddleware(), $handler -)); +); ``` > Internally, this class automatically assigns these middleware handlers @@ -822,10 +823,11 @@ also use a streaming approach where only small chunks of data have to be kept in memory: ```php -$server = new React\Http\Server($loop, array( +$server = new React\Http\Server( + $loop, new React\Http\Middleware\StreamingRequestMiddleware(), $handler -)); +); ``` In this case, it will invoke the request handler function once the HTTP @@ -1141,7 +1143,8 @@ The ReactPHP `ReadableStreamInterface` gives you access to the incoming request body as the individual chunks arrive: ```php -$server = new React\Http\Server($loop, array( +$server = new React\Http\Server( + $loop, new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $body = $request->getBody(); @@ -1176,7 +1179,7 @@ $server = new React\Http\Server($loop, array( }); }); } -)); +); ``` The above example simply counts the number of bytes received in the request body. @@ -1214,7 +1217,8 @@ This method operates on the streaming request body, i.e. the request body size may be unknown (`null`) when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. ```php -$server = new React\Http\Server($loop, array( +$server = new React\Http\Server( + $loop, new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $size = $request->getBody()->getSize(); @@ -1239,7 +1243,7 @@ $server = new React\Http\Server($loop, array( "Request body size: " . $size . " bytes\n" ); } -)); +); ``` > Note: The `Server` automatically takes care of handling requests with the @@ -1723,7 +1727,8 @@ The following example adds a middleware request handler that adds the current ti header (`Request-Time`) and a final request handler that always returns a 200 code without a body: ```php -$server = new Server($loop, array( +$server = new Server( + $loop, function (ServerRequestInterface $request, callable $next) { $request = $request->withHeader('Request-Time', time()); return $next($request); @@ -1731,7 +1736,7 @@ $server = new Server($loop, array( function (ServerRequestInterface $request) { return new Response(200); } -)); +); ``` > Note how the middleware request handler and the final request handler have a @@ -1747,7 +1752,8 @@ In order to simplify handling both paths, you can simply wrap this in a [`Promise\resolve()`](https://reactphp.org/promise/#resolve) call like this: ```php -$server = new Server($loop, array( +$server = new Server( + $loop, function (ServerRequestInterface $request, callable $next) { $promise = React\Promise\resolve($next($request)); return $promise->then(function (ResponseInterface $response) { @@ -1757,7 +1763,7 @@ $server = new Server($loop, array( function (ServerRequestInterface $request) { return new Response(200); } -)); +); ``` Note that the `$next` middleware request handler may also throw an @@ -1769,7 +1775,8 @@ handling logic (or logging etc.) by wrapping this in a [`Promise`](https://reactphp.org/promise/#promise) like this: ```php -$server = new Server($loop, array( +$server = new Server( + $loop, function (ServerRequestInterface $request, callable $next) { $promise = new React\Promise\Promise(function ($resolve) use ($next, $request) { $resolve($next($request)); @@ -1788,7 +1795,7 @@ $server = new Server($loop, array( } return new Response(200); } -)); +); ``` #### Third-Party Middleware @@ -2404,10 +2411,11 @@ The following example shows how this middleware can be used to ensure no more than 10 handlers will be invoked at once: ```php -$server = new Server($loop, array( +$server = new Server( + $loop, new LimitConcurrentRequestsMiddleware(10), $handler -)); +); ``` Similarly, this middleware is often used in combination with the @@ -2415,13 +2423,14 @@ Similarly, this middleware is often used in combination with the to limit the total number of requests that can be buffered at once: ```php -$server = new Server($loop, array( +$server = new Server( + $loop, new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request new RequestBodyParserMiddleware(), $handler -)); +); ``` More sophisticated examples include limiting the total number of requests @@ -2429,14 +2438,15 @@ that can be buffered at once and then ensure the actual request handler only processes one request after another without any concurrency: ```php -$server = new Server($loop, array( +$server = new Server( + $loop, new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request new RequestBodyParserMiddleware(), new LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency) $handler -)); +); ``` #### RequestBodyBufferMiddleware @@ -2482,7 +2492,8 @@ the total number of concurrent requests. Usage: ```php -$server = new Server($loop, array( +$server = new Server( + $loop, new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB @@ -2490,7 +2501,7 @@ $server = new Server($loop, array( // The body from $request->getBody() is now fully available without the need to stream it return new Response(200); }, -)); +); ``` #### RequestBodyParserMiddleware @@ -2542,13 +2553,14 @@ $handler = function (ServerRequestInterface $request) { ); }; -$server = new Server($loop, array( +$server = new Server( + $loop, new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB new RequestBodyParserMiddleware(), $handler -)); +); ``` See also [form upload server example](examples/62-server-form-upload.php) for more details. diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index 4db8c5d7..85d948fc 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -124,13 +124,14 @@ // Note how this example explicitly uses the advanced `StreamingRequestMiddleware` to apply // custom request buffering limits below before running our request handler. -$server = new Server($loop, array( +$server = new Server( + $loop, new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers, queue otherwise new RequestBodyBufferMiddleware(8 * 1024 * 1024), // 8 MiB max, ignore body otherwise new RequestBodyParserMiddleware(100 * 1024, 1), // 1 file with 100 KiB max, reject upload otherwise $handler -)); +); $socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); $server->listen($socket); diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 4c20e39a..3ec791b2 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -95,19 +95,17 @@ final class StreamingServer extends EventEmitter * See also [listen()](#listen) for more details. * * @param LoopInterface $loop - * @param callable|callable[] $requestHandler + * @param callable $requestHandler * @see self::listen() */ public function __construct(LoopInterface $loop, $requestHandler) { - $this->loop = $loop; - - if (!\is_callable($requestHandler) && !\is_array($requestHandler)) { + if (!\is_callable($requestHandler)) { throw new \InvalidArgumentException('Invalid request handler given'); - } elseif (!\is_callable($requestHandler)) { - $requestHandler = new MiddlewareRunner($requestHandler); } + $this->loop = $loop; + $this->callback = $requestHandler; $this->parser = new RequestHeaderParser(); diff --git a/src/Server.php b/src/Server.php index ee1f1f65..68ce3542 100644 --- a/src/Server.php +++ b/src/Server.php @@ -5,6 +5,7 @@ use Evenement\EventEmitter; use React\EventLoop\LoopInterface; use React\Http\Io\IniUtil; +use React\Http\Io\MiddlewareRunner; use React\Http\Io\StreamingServer; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\StreamingRequestMiddleware; @@ -166,17 +167,20 @@ final class Server extends EventEmitter * See also [listen()](#listen) for more details. * * @param LoopInterface $loop - * @param callable|callable[] $requestHandler + * @param callable[] ...$requestHandler * @see self::listen() */ - public function __construct(LoopInterface $loop, $requestHandler) + public function __construct(LoopInterface $loop) { - if (!\is_callable($requestHandler) && !\is_array($requestHandler)) { + $requestHandlers = \func_get_args(); + \array_shift($requestHandlers); + $requestHandlersCount = \count($requestHandlers); + if ($requestHandlersCount === 0 || \count(\array_filter($requestHandlers, 'is_callable')) < $requestHandlersCount) { throw new \InvalidArgumentException('Invalid request handler given'); } $streaming = false; - foreach ((array) $requestHandler as $handler) { + foreach ((array) $requestHandlers as $handler) { if ($handler instanceof StreamingRequestMiddleware) { $streaming = true; break; @@ -200,13 +204,9 @@ public function __construct(LoopInterface $loop, $requestHandler) } } - if (\is_callable($requestHandler)) { - $middleware[] = $requestHandler; - } else { - $middleware = \array_merge($middleware, $requestHandler); - } + $middleware = \array_merge($middleware, $requestHandlers); - $this->streamingServer = new StreamingServer($loop, $middleware); + $this->streamingServer = new StreamingServer($loop, new MiddlewareRunner($middleware)); $that = $this; $this->streamingServer->on('error', function ($error) use ($that) { diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index de5a9f9a..69c60ba7 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -32,7 +32,7 @@ public function setUpBrowserAndServer() $this->loop = $loop = Factory::create(); $this->browser = new Browser($this->loop); - $server = new Server($this->loop, array(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use ($loop) { + $server = new Server($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use ($loop) { $path = $request->getUri()->getPath(); $headers = array(); @@ -132,7 +132,7 @@ public function setUpBrowserAndServer() } var_dump($path); - })); + }); $socket = new \React\Socket\Server(0, $this->loop); $server->listen($socket); @@ -527,9 +527,9 @@ public function testPostStreamKnownLength() */ public function testPostStreamWillStartSendingRequestEvenWhenBodyDoesNotEmitData() { - $server = new Server($this->loop, array(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { + $server = new Server($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { return new Response(200); - })); + }); $socket = new \React\Socket\Server(0, $this->loop); $server->listen($socket); diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index f92fb2b0..ba834082 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -52,11 +52,12 @@ public function testPlainHttpOnRandomPortWithSingleRequestHandlerArray() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, array( + $server = new Server( + $loop, function () { return new Response(404); - }, - )); + } + ); $socket = new Socket(0, $loop); $server->listen($socket); @@ -479,12 +480,13 @@ public function testRequestHandlerWithStreamingRequestWillReceiveCloseEventIfCon $connector = new Connector($loop); $once = $this->expectCallableOnce(); - $server = new Server($loop, array( + $server = new Server( + $loop, new StreamingRequestMiddleware(), function (RequestInterface $request) use ($once) { $request->getBody()->on('close', $once); } - )); + ); $socket = new Socket(0, $loop); $server->listen($socket); @@ -509,12 +511,13 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileS $stream = new ThroughStream(); - $server = new Server($loop, array( + $server = new Server( + $loop, new StreamingRequestMiddleware(), function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); } - )); + ); $socket = new Socket(0, $loop); $server->listen($socket); @@ -757,7 +760,8 @@ public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, array( + $server = new Server( + $loop, new LimitConcurrentRequestsMiddleware(5), new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (ServerRequestInterface $request, $next) use ($loop) { @@ -770,7 +774,7 @@ function (ServerRequestInterface $request, $next) use ($loop) { function (ServerRequestInterface $request) { return new Response(200, array(), (string)strlen((string)$request->getBody())); } - )); + ); $socket = new Socket(0, $loop); $server->listen($socket); diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 3559c1d2..b903a850 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -127,10 +127,10 @@ public function testRequestEventWithSingleRequestHandlerArray() { $i = 0; $requestAssertion = null; - $server = new StreamingServer(Factory::create(), array(function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; - })); + }); $this->connection ->expects($this->any()) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 8d8cac5d..59452815 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -88,7 +88,8 @@ public function helperCallableOnce() public function testSimpleRequestWithMiddlewareArrayProcessesMiddlewareStack() { $called = null; - $server = new Server(Factory::create(), array( + $server = new Server( + Factory::create(), function (ServerRequestInterface $request, $next) use (&$called) { $called = 'before'; $ret = $next($request->withHeader('Demo', 'ok')); @@ -99,7 +100,7 @@ function (ServerRequestInterface $request, $next) use (&$called) { function (ServerRequestInterface $request) use (&$called) { $called .= $request->getHeaderLine('Demo'); } - )); + ); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -161,12 +162,13 @@ public function testServerReceivesBufferedRequestByDefault() public function testServerWithStreamingRequestMiddlewareReceivesStreamingRequest() { $streaming = null; - $server = new Server(Factory::create(), array( + $server = new Server( + Factory::create(), new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use (&$streaming) { $streaming = $request->getBody() instanceof ReadableStreamInterface; } - )); + ); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); From 49b4799d56895b79cc3e1fa05b070f06674b5979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 10 Jul 2020 12:40:31 +0200 Subject: [PATCH 024/152] Improve default concurrency and cap default request buffer at 64K --- README.md | 41 +++++-- src/Io/StreamingServer.php | 4 +- .../LimitConcurrentRequestsMiddleware.php | 15 ++- src/Server.php | 100 +++++++++------ tests/ServerTest.php | 116 ++++++++++++++++-- 5 files changed, 208 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index f95214e5..2888e500 100644 --- a/README.md +++ b/README.md @@ -773,7 +773,8 @@ their respective default values: ``` memory_limit 128M -post_max_size 8M +post_max_size 8M // capped at 64K + enable_post_data_reading 1 max_input_nesting_level 64 max_input_vars 1000 @@ -784,22 +785,30 @@ max_file_uploads 20 ``` In particular, the `post_max_size` setting limits how much memory a single -HTTP request is allowed to consume while buffering its request body. On top -of this, this class will try to avoid consuming more than 1/4 of your +HTTP request is allowed to consume while buffering its request body. This +needs to be limited because the server can process a large number of requests +concurrently, so the server may potentially consume a large amount of memory +otherwise. To support higher concurrency by default, this value is capped +at `64K`. If you assign a higher value, it will only allow `64K` by default. +If a request exceeds this limit, its request body will be ignored and it will +be processed like a request with no request body at all. See below for +explicit configuration to override this setting. + +By default, this class will try to avoid consuming more than half of your `memory_limit` for buffering multiple concurrent HTTP requests. As such, with the above default settings of `128M` max, it will try to consume no more than -`32M` for buffering multiple concurrent HTTP requests. As a consequence, it -will limit the concurrency to 4 HTTP requests with the above defaults. +`64M` for buffering multiple concurrent HTTP requests. As a consequence, it +will limit the concurrency to `1024` HTTP requests with the above defaults. It is imperative that you assign reasonable values to your PHP ini settings. -It is usually recommended to either reduce the memory a single request is -allowed to take (set `post_max_size 1M` or less) or to increase the total -memory limit to allow for more concurrent requests (set `memory_limit 512M` -or more). Failure to do so means that this class may have to disable -concurrency and only handle one request at a time. - -As an alternative to the above buffering defaults, you can also configure -the `Server` explicitly to override these defaults. You can use the +It is usually recommended to not support buffering incoming HTTP requests +with a large HTTP request body (e.g. large file uploads). If you want to +increase this buffer size, you will have to also increase the total memory +limit to allow for more concurrent requests (set `memory_limit 512M` or more) +or explicitly limit concurrency. + +In order to override the above buffering defaults, you can configure the +`Server` explicitly. You can use the [`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) to explicitly configure the total number of requests that can be handled at @@ -816,6 +825,12 @@ $server = new React\Http\Server( ); ``` +In this example, we allow processing up to 100 concurrent requests at once +and each request can buffer up to `2M`. This means you may have to keep a +maximum of `200M` of memory for incoming request body buffers. Accordingly, +you need to adjust the `memory_limit` ini setting to allow for these buffers +plus your actual application logic memory requirements (think `512M` or more). + > Internally, this class automatically assigns these middleware handlers automatically when no [`StreamingRequestMiddleware`](#streamingrequestmiddleware) is given. Accordingly, you can use this example to override all default diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 1d41a9c3..166cd4e0 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -29,7 +29,7 @@ * object in return: * * ```php - * $server = new StreamingServer(function (ServerRequestInterface $request) { + * $server = new StreamingServer($loop, function (ServerRequestInterface $request) { * return new Response( * 200, * array( @@ -54,7 +54,7 @@ * in order to start a plaintext HTTP server like this: * * ```php - * $server = new StreamingServer($handler); + * $server = new StreamingServer($loop, $handler); * * $socket = new React\Socket\Server('0.0.0.0:8080', $loop); * $server->listen($socket); diff --git a/src/Middleware/LimitConcurrentRequestsMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php index d16402df..d6760e95 100644 --- a/src/Middleware/LimitConcurrentRequestsMiddleware.php +++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -29,11 +29,12 @@ * than 10 handlers will be invoked at once: * * ```php - * $server = new Server(array( + * $server = new Server( + * $loop, * new StreamingRequestMiddleware(), * new LimitConcurrentRequestsMiddleware(10), * $handler - * )); + * ); * ``` * * Similarly, this middleware is often used in combination with the @@ -41,13 +42,14 @@ * to limit the total number of requests that can be buffered at once: * * ```php - * $server = new Server(array( + * $server = new Server( + * $loop, * new StreamingRequestMiddleware(), * new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request * new RequestBodyParserMiddleware(), * $handler - * )); + * ); * ``` * * More sophisticated examples include limiting the total number of requests @@ -55,14 +57,15 @@ * processes one request after another without any concurrency: * * ```php - * $server = new Server(array( + * $server = new Server( + * $loop, * new StreamingRequestMiddleware(), * new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request * new RequestBodyParserMiddleware(), * new LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency) * $handler - * )); + * ); * ``` * * @see RequestBodyBufferMiddleware diff --git a/src/Server.php b/src/Server.php index 7b165c37..3b5d70be 100644 --- a/src/Server.php +++ b/src/Server.php @@ -23,7 +23,7 @@ * object and expects a [response](#server-response) object in return: * * ```php - * $server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { + * $server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { * return new React\Http\Message\Response( * 200, * array( @@ -51,7 +51,7 @@ * to start a plaintext HTTP server like this: * * ```php - * $server = new React\Http\Server($handler); + * $server = new React\Http\Server($loop, $handler); * * $socket = new React\Socket\Server('0.0.0.0:8080', $loop); * $server->listen($socket); @@ -79,7 +79,8 @@ * * ``` * memory_limit 128M - * post_max_size 8M + * post_max_size 8M // capped at 64K + * * enable_post_data_reading 1 * max_input_nesting_level 64 * max_input_vars 1000 @@ -90,29 +91,38 @@ * ``` * * In particular, the `post_max_size` setting limits how much memory a single - * HTTP request is allowed to consume while buffering its request body. On top - * of this, this class will try to avoid consuming more than 1/4 of your + * HTTP request is allowed to consume while buffering its request body. This + * needs to be limited because the server can process a large number of requests + * concurrently, so the server may potentially consume a large amount of memory + * otherwise. To support higher concurrency by default, this value is capped + * at `64K`. If you assign a higher value, it will only allow `64K` by default. + * If a request exceeds this limit, its request body will be ignored and it will + * be processed like a request with no request body at all. See below for + * explicit configuration to override this setting. + * + * By default, this class will try to avoid consuming more than half of your * `memory_limit` for buffering multiple concurrent HTTP requests. As such, with * the above default settings of `128M` max, it will try to consume no more than - * `32M` for buffering multiple concurrent HTTP requests. As a consequence, it - * will limit the concurrency to 4 HTTP requests with the above defaults. + * `64M` for buffering multiple concurrent HTTP requests. As a consequence, it + * will limit the concurrency to `1024` HTTP requests with the above defaults. * * It is imperative that you assign reasonable values to your PHP ini settings. - * It is usually recommended to either reduce the memory a single request is - * allowed to take (set `post_max_size 1M` or less) or to increase the total - * memory limit to allow for more concurrent requests (set `memory_limit 512M` - * or more). Failure to do so means that this class may have to disable - * concurrency and only handle one request at a time. + * It is usually recommended to not support buffering incoming HTTP requests + * with a large HTTP request body (e.g. large file uploads). If you want to + * increase this buffer size, you will have to also increase the total memory + * limit to allow for more concurrent requests (set `memory_limit 512M` or more) + * or explicitly limit concurrency. * - * As an alternative to the above buffering defaults, you can also configure - * the `Server` explicitly to override these defaults. You can use the + * In order to override the above buffering defaults, you can configure the + * `Server` explicitly. You can use the * [`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and * [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) * to explicitly configure the total number of requests that can be handled at * once like this: * * ```php - * $server = new React\Http\Server(array( + * $server = new React\Http\Server( + * $loop, * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -121,6 +131,12 @@ * )); * ``` * + * In this example, we allow processing up to 100 concurrent requests at once + * and each request can buffer up to `2M`. This means you may have to keep a + * maximum of `200M` of memory for incoming request body buffers. Accordingly, + * you need to adjust the `memory_limit` ini setting to allow for these buffers + * plus your actual application logic memory requirements (think `512M` or more). + * * > Internally, this class automatically assigns these middleware handlers * automatically when no [`StreamingRequestMiddleware`](#streamingrequestmiddleware) * is given. Accordingly, you can use this example to override all default @@ -131,10 +147,11 @@ * in memory: * * ```php - * $server = new React\Http\Server(array( + * $server = new React\Http\Server( + * $loop, * new React\Http\Middleware\StreamingRequestMiddleware(), * $handler - * )); + * ); * ``` * * In this case, it will invoke the request handler function once the HTTP @@ -149,9 +166,17 @@ final class Server extends EventEmitter { /** + * The maximum buffer size used for each request. + * + * This needs to be limited because the server can process a large number of + * requests concurrently, so the server may potentially consume a large + * amount of memory otherwise. + * + * See `RequestBodyBufferMiddleware` to override this setting. + * * @internal */ - const MAXIMUM_CONCURRENT_REQUESTS = 100; + const MAXIMUM_BUFFER_SIZE = 65536; // 64 KiB /** * @var StreamingServer @@ -189,10 +214,12 @@ public function __construct(LoopInterface $loop) $middleware = array(); if (!$streaming) { - $middleware[] = new LimitConcurrentRequestsMiddleware( - $this->getConcurrentRequestsLimit(\ini_get('memory_limit'), \ini_get('post_max_size')) - ); - $middleware[] = new RequestBodyBufferMiddleware(); + $maxSize = $this->getMaxRequestSize(); + $concurrency = $this->getConcurrentRequestsLimit(\ini_get('memory_limit'), $maxSize); + if ($concurrency !== null) { + $middleware[] = new LimitConcurrentRequestsMiddleware($concurrency); + } + $middleware[] = new RequestBodyBufferMiddleware($maxSize); // Checking for an empty string because that is what a boolean // false is returned as by ini_get depending on the PHP version. // @link http://php.net/manual/en/ini.core.php#ini.enable-post-data-reading @@ -226,7 +253,7 @@ public function __construct(LoopInterface $loop) * order to start a plaintext HTTP server like this: * * ```php - * $server = new React\Http\Server($handler); + * $server = new React\Http\Server($loop, $handler); * * $socket = new React\Socket\Server(8080, $loop); * $server->listen($socket); @@ -252,7 +279,7 @@ public function __construct(LoopInterface $loop) * `passphrase` like this: * * ```php - * $server = new React\Http\Server($handler); + * $server = new React\Http\Server($loop, $handler); * * $socket = new React\Socket\Server('tls://0.0.0.0:8443', $loop, array( * 'local_cert' => __DIR__ . '/localhost.pem' @@ -273,25 +300,28 @@ public function listen(ServerInterface $server) /** * @param string $memory_limit * @param string $post_max_size - * @return int + * @return ?int */ private function getConcurrentRequestsLimit($memory_limit, $post_max_size) { if ($memory_limit == -1) { - return self::MAXIMUM_CONCURRENT_REQUESTS; - } - - if ($post_max_size == 0) { - return 1; + return null; } - $availableMemory = IniUtil::iniSizeToBytes($memory_limit) / 4; + $availableMemory = IniUtil::iniSizeToBytes($memory_limit) / 2; $concurrentRequests = (int) \ceil($availableMemory / IniUtil::iniSizeToBytes($post_max_size)); - if ($concurrentRequests >= self::MAXIMUM_CONCURRENT_REQUESTS) { - return self::MAXIMUM_CONCURRENT_REQUESTS; - } - return $concurrentRequests; } + + /** + * @param ?string $post_max_size + * @return int + */ + private function getMaxRequestSize($post_max_size = null) + { + $maxSize = IniUtil::iniSizeToBytes($post_max_size === null ? \ini_get('post_max_size') : $post_max_size); + + return ($maxSize === 0 || $maxSize >= self::MAXIMUM_BUFFER_SIZE) ? self::MAXIMUM_BUFFER_SIZE : $maxSize; + } } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 59452815..ce19dda9 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -10,6 +10,7 @@ use React\Promise; use React\Http\Middleware\StreamingRequestMiddleware; use React\Stream\ReadableStreamInterface; +use React\Http\Io\IniUtil; final class ServerTest extends TestCase { @@ -224,23 +225,18 @@ public function provideIniSettingsForConcurrency() return array( 'default settings' => array( '128M', - '8M', - 4 + '64K', // 8M capped at maximum + 1024 ), - 'unlimited memory_limit limited to maximum concurrency' => array( + 'unlimited memory_limit has no concurrency limit' => array( '-1', '8M', - 100 - ), - 'unlimited post_max_size' => array( - '128M', - '0', - 1 + null ), - 'small post_max_size limited to maximum concurrency' => array( + 'small post_max_size results in high concurrency' => array( '128M', '1k', - 100 + 65536 ) ); } @@ -248,7 +244,7 @@ public function provideIniSettingsForConcurrency() /** * @param string $memory_limit * @param string $post_max_size - * @param int $expectedConcurrency + * @param ?int $expectedConcurrency * @dataProvider provideIniSettingsForConcurrency */ public function testServerConcurrency($memory_limit, $post_max_size, $expectedConcurrency) @@ -262,4 +258,100 @@ public function testServerConcurrency($memory_limit, $post_max_size, $expectedCo $this->assertEquals($expectedConcurrency, $value); } + + public function testServerGetPostMaxSizeReturnsSizeFromGivenIniSetting() + { + $server = new Server(Factory::create(), function () { }); + + $ref = new \ReflectionMethod($server, 'getMaxRequestSize'); + $ref->setAccessible(true); + + $value = $ref->invoke($server, '1k'); + + $this->assertEquals(1024, $value); + } + + public function testServerGetPostMaxSizeReturnsSizeCappedFromGivenIniSetting() + { + $server = new Server(Factory::create(), function () { }); + + $ref = new \ReflectionMethod($server, 'getMaxRequestSize'); + $ref->setAccessible(true); + + $value = $ref->invoke($server, '1M'); + + $this->assertEquals(64 * 1024, $value); + } + + public function testServerGetPostMaxSizeFromIniIsCapped() + { + if (IniUtil::iniSizeToBytes(ini_get('post_max_size')) < 64 * 1024) { + $this->markTestSkipped(); + } + + $server = new Server(Factory::create(), function () { }); + + $ref = new \ReflectionMethod($server, 'getMaxRequestSize'); + $ref->setAccessible(true); + + $value = $ref->invoke($server); + + $this->assertEquals(64 * 1024, $value); + } + + public function testConstructServerWithUnlimitedMemoryLimitDoesNotLimitConcurrency() + { + $old = ini_get('memory_limit'); + ini_set('memory_limit', '-1'); + + $server = new Server(Factory::create(), function () { }); + + ini_set('memory_limit', $old); + + $ref = new \ReflectionProperty($server, 'streamingServer'); + $ref->setAccessible(true); + + $streamingServer = $ref->getValue($server); + + $ref = new \ReflectionProperty($streamingServer, 'callback'); + $ref->setAccessible(true); + + $middlewareRunner = $ref->getValue($streamingServer); + + $ref = new \ReflectionProperty($middlewareRunner, 'middleware'); + $ref->setAccessible(true); + + $middleware = $ref->getValue($middlewareRunner); + + $this->assertTrue(is_array($middleware)); + $this->assertInstanceOf('React\Http\Middleware\RequestBodyBufferMiddleware', $middleware[0]); + } + + public function testConstructServerWithMemoryLimitDoesLimitConcurrency() + { + $old = ini_get('memory_limit'); + ini_set('memory_limit', '100M'); + + $server = new Server(Factory::create(), function () { }); + + ini_set('memory_limit', $old); + + $ref = new \ReflectionProperty($server, 'streamingServer'); + $ref->setAccessible(true); + + $streamingServer = $ref->getValue($server); + + $ref = new \ReflectionProperty($streamingServer, 'callback'); + $ref->setAccessible(true); + + $middlewareRunner = $ref->getValue($streamingServer); + + $ref = new \ReflectionProperty($middlewareRunner, 'middleware'); + $ref->setAccessible(true); + + $middleware = $ref->getValue($middlewareRunner); + + $this->assertTrue(is_array($middleware)); + $this->assertInstanceOf('React\Http\Middleware\LimitConcurrentRequestsMiddleware', $middleware[0]); + } } From b7a9251428c06cb16294aa785cf0184a44ce768a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 10 Jul 2020 13:08:34 +0200 Subject: [PATCH 025/152] Support skipping all online tests with --exclude-group internet --- README.md | 8 ++++++++ tests/Client/FunctionalIntegrationTest.php | 8 ++++++++ tests/FunctionalBrowserTest.php | 20 ++++++++++---------- tests/Io/RequestHeaderParserTest.php | 6 ------ 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index f95214e5..9de7bcd5 100644 --- a/README.md +++ b/README.md @@ -2741,6 +2741,14 @@ 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 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/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index d6cc4b0f..db82b1f1 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -101,6 +101,10 @@ public function testSuccessfulResponseEmitsEnd() /** @group internet */ public function testPostDataReturnsData() { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); + } + $loop = Factory::create(); $client = new Client($loop); @@ -130,6 +134,10 @@ public function testPostDataReturnsData() /** @group internet */ public function testPostJsonReturnsData() { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); + } + $loop = Factory::create(); $client = new Client($loop); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 3cf9293a..f4495565 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -351,25 +351,25 @@ public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() } /** - * @group online + * @group internet * @doesNotPerformAssertions */ public function testCanAccessHttps() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); } Block\await($this->browser->get('https://www.google.com/'), $this->loop); } /** - * @group online + * @group internet */ public function testVerifyPeerEnabledForBadSslRejects() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); } $connector = new Connector($this->loop, array( @@ -385,13 +385,13 @@ public function testVerifyPeerEnabledForBadSslRejects() } /** - * @group online + * @group internet * @doesNotPerformAssertions */ public function testVerifyPeerDisabledForBadSslResolves() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); } $connector = new Connector($this->loop, array( @@ -406,7 +406,7 @@ public function testVerifyPeerDisabledForBadSslResolves() } /** - * @group online + * @group internet */ public function testInvalidPort() { diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index ca18df13..b0a339ed 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -340,9 +340,6 @@ public function testInvalidMalformedRequestLineParseException() $this->assertSame('Unable to parse invalid request-line', $error->getMessage()); } - /** - * @group a - */ public function testInvalidMalformedRequestHeadersThrowsParseException() { $error = null; @@ -362,9 +359,6 @@ public function testInvalidMalformedRequestHeadersThrowsParseException() $this->assertSame('Unable to parse invalid request header fields', $error->getMessage()); } - /** - * @group a - */ public function testInvalidMalformedRequestHeadersWhitespaceThrowsParseException() { $error = null; From 89a1d418973ecb9d340b9af4a95f63d0cd9d0818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 10 Jul 2020 21:44:32 +0200 Subject: [PATCH 026/152] Expose ReactPHP in `User-Agent` and `Server` header The `Browser` now sends a `User-Agent: ReactPHP/1` request header (was `User-Agent: React/alpha`) by default. The `Server` now sends a `Server: ReactPHP/1` response header (was `X-Powered-By: React/alpha`) by default. Both can be overridden by explicitly assigning other header values as usual. --- README.md | 51 ++++++++++++++++++-------------- src/Client/RequestData.php | 2 +- src/Io/StreamingServer.php | 10 +++---- tests/Client/RequestDataTest.php | 16 +++++----- tests/Io/StreamingServerTest.php | 14 ++++----- 5 files changed, 49 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index f95214e5..fe82f20b 100644 --- a/README.md +++ b/README.md @@ -1611,65 +1611,70 @@ create your own HTTP response message instead. When a response is returned from the request handler function, it will be processed by the [`Server`](#server) and then sent back to the client. -The `Server` will automatically add the protocol version of the request, so you -don't have to. - -A `Date` header will be automatically added with the system date and time if none is given. -You can add a custom `Date` header yourself like this: +A `Server: ReactPHP/1` response header will be added automatically. You can add +a custom `Server` response header like this: ```php -$server = new Server($loop, function (ServerRequestInterface $request) { - return new Response( +$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { + return new React\Http\Message\Response( 200, array( - 'Date' => date('D, d M Y H:i:s T') + 'Server' => 'PHP/3' ) ); }); ``` -If you don't have a appropriate clock to rely on, you should -unset this header with an empty string: +If you do not want to send this `Sever` response header at all (such as when you +don't want to expose the underlying server software), you can use an empty +string value like this: ```php -$server = new Server($loop, function (ServerRequestInterface $request) { - return new Response( +$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { + return new React\Http\Message\Response( 200, array( - 'Date' => '' + 'Server' => '' ) ); }); ``` -Note that it will automatically assume a `X-Powered-By: react/alpha` header -unless your specify a custom `X-Powered-By` header yourself: +A `Date` response header will be added automatically with the current system +date and time if none is given. You can add a custom `Date` response header +like this: ```php -$server = new Server($loop, function (ServerRequestInterface $request) { - return new Response( +$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { + return new React\Http\Message\Response( 200, array( - 'X-Powered-By' => 'PHP 3' + 'Date' => gmdate('D, d M Y H:i:s \G\M\T') ) ); }); ``` -If you do not want to send this header at all, you can use an empty string as -value like this: +If you do not want to send this `Date` response header at all (such as when you +don't have an appropriate clock to rely on), you can use an empty string value +like this: ```php -$server = new Server($loop, function (ServerRequestInterface $request) { - return new Response( +$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { + return new React\Http\Message\Response( 200, array( - 'X-Powered-By' => '' + 'Date' => '' ) ); }); ``` +The `Server` class will automatically add the protocol version of the request, +so you don't have to. For instance, if the client sends the request using the +HTTP/1.1 protocol version, the response message will also use the same protocol +version, no matter what version is returned from the request handler function. + Note that persistent connections (`Connection: keep-alive`) are currently not supported. As such, HTTP/1.1 response messages will automatically include a diff --git a/src/Client/RequestData.php b/src/Client/RequestData.php index 55efaa9b..a5908a08 100644 --- a/src/Client/RequestData.php +++ b/src/Client/RequestData.php @@ -29,7 +29,7 @@ private function mergeDefaultheaders(array $headers) $defaults = array_merge( array( 'Host' => $this->getHost().$port, - 'User-Agent' => 'React/alpha', + 'User-Agent' => 'ReactPHP/1', ), $connectionHeaders, $authHeaders diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 1d41a9c3..c056ac56 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -244,11 +244,11 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt $version = $request->getProtocolVersion(); $response = $response->withProtocolVersion($version); - // assign default "X-Powered-By" header automatically - if (!$response->hasHeader('X-Powered-By')) { - $response = $response->withHeader('X-Powered-By', 'React/alpha'); - } elseif ($response->getHeaderLine('X-Powered-By') === ''){ - $response = $response->withoutHeader('X-Powered-By'); + // assign default "Server" header automatically + if (!$response->hasHeader('Server')) { + $response = $response->withHeader('Server', 'ReactPHP/1'); + } elseif ($response->getHeaderLine('Server') === ''){ + $response = $response->withoutHeader('Server'); } // assign default "Date" header from current time automatically diff --git a/tests/Client/RequestDataTest.php b/tests/Client/RequestDataTest.php index 313e140f..7f96e152 100644 --- a/tests/Client/RequestDataTest.php +++ b/tests/Client/RequestDataTest.php @@ -14,7 +14,7 @@ public function toStringReturnsHTTPRequestMessage() $expected = "GET / HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: React/alpha\r\n" . + "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -27,7 +27,7 @@ public function toStringReturnsHTTPRequestMessageWithEmptyQueryString() $expected = "GET /path?hello=world HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: React/alpha\r\n" . + "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -40,7 +40,7 @@ public function toStringReturnsHTTPRequestMessageWithZeroQueryStringAndRootPath( $expected = "GET /?0 HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: React/alpha\r\n" . + "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -53,7 +53,7 @@ public function toStringReturnsHTTPRequestMessageWithOptionsAbsoluteRequestForm( $expected = "OPTIONS / HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: React/alpha\r\n" . + "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -66,7 +66,7 @@ public function toStringReturnsHTTPRequestMessageWithOptionsAsteriskRequestForm( $expected = "OPTIONS * HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: React/alpha\r\n" . + "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -80,7 +80,7 @@ public function toStringReturnsHTTPRequestMessageWithProtocolVersion() $expected = "GET / HTTP/1.1\r\n" . "Host: www.example.com\r\n" . - "User-Agent: React/alpha\r\n" . + "User-Agent: ReactPHP/1\r\n" . "Connection: close\r\n" . "\r\n"; @@ -131,7 +131,7 @@ public function toStringReturnsHTTPRequestMessageWithProtocolVersionThroughConst $expected = "GET / HTTP/1.1\r\n" . "Host: www.example.com\r\n" . - "User-Agent: React/alpha\r\n" . + "User-Agent: ReactPHP/1\r\n" . "Connection: close\r\n" . "\r\n"; @@ -145,7 +145,7 @@ public function toStringUsesUserPassFromURL() $expected = "GET / HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: React/alpha\r\n" . + "User-Agent: ReactPHP/1\r\n" . "Authorization: Basic am9objpkdW1teQ==\r\n" . "\r\n"; diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 5dff1f0c..d2401a06 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -650,7 +650,7 @@ public function testRequestEventWithPartialBodyWillEmitData() $this->connection->emit('data', array($data)); } - public function testResponseContainsPoweredByHeader() + public function testResponseContainsServerHeader() { $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { return new Response(); @@ -675,7 +675,7 @@ function ($data) use (&$buffer) { $data = $this->createGetRequest(); $this->connection->emit('data', array($data)); - $this->assertContainsString("\r\nX-Powered-By: React/alpha\r\n", $buffer); + $this->assertContainsString("\r\nServer: ReactPHP/1\r\n", $buffer); } public function testResponsePendingPromiseWillNotSendAnything() @@ -931,7 +931,7 @@ public function testResponseUpgradeInResponseCanBeUsedToAdvertisePossibleUpgrade 200, array( 'date' => '', - 'x-powered-by' => '', + 'server' => '', 'Upgrade' => 'demo' ), 'foo' @@ -967,7 +967,7 @@ public function testResponseUpgradeWishInRequestCanBeIgnoredByReturningNormalRes 200, array( 'date' => '', - 'x-powered-by' => '' + 'server' => '' ), 'foo' ); @@ -1002,7 +1002,7 @@ public function testResponseUpgradeSwitchingProtocolIncludesConnectionUpgradeHea 101, array( 'date' => '', - 'x-powered-by' => '', + 'server' => '', 'Upgrade' => 'demo' ), 'foo' @@ -1042,7 +1042,7 @@ public function testResponseUpgradeSwitchingProtocolWithStreamWillPipeDataToConn 101, array( 'date' => '', - 'x-powered-by' => '', + 'server' => '', 'Upgrade' => 'demo' ), $stream @@ -2171,7 +2171,7 @@ public function testResponseCanContainMultipleCookieHeaders() 'session=abc' ), 'Date' => '', - 'X-Powered-By' => '' + 'Server' => '' ) ); }); From b9ed1e62d7423da97e8d2fbc38f2ce2761951ea2 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Fri, 10 Jul 2020 19:27:52 +0200 Subject: [PATCH 027/152] Tag Browser as final to discourage inheriting from it --- README.md | 2 ++ src/Browser.php | 3 +++ 2 files changed, 5 insertions(+) diff --git a/README.md b/README.md index f95214e5..1026206f 100644 --- a/README.md +++ b/README.md @@ -1866,6 +1866,8 @@ $connector = new React\Socket\Connector($loop, array( $browser = new React\Http\Browser($loop, $connector); ``` +> Note that the browser class is final and shouldn't be extended, it is likely to be marked final in a future release. + #### get() The `get(string $url, array $headers = array()): PromiseInterface` method can be used to diff --git a/src/Browser.php b/src/Browser.php index 28f90f87..9da950ba 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -13,6 +13,9 @@ use React\Socket\ConnectorInterface; use React\Stream\ReadableStreamInterface; +/** + * @final This class is final and shouldn't be extended as it is likely to be marked final in a future relase. + */ class Browser { private $transaction; From c3e3da687a8e6709ba1094f84f54ddd92ecf9824 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Fri, 10 Jul 2020 22:10:44 +0200 Subject: [PATCH 028/152] Mark all non-internal Message classes final --- src/Message/Response.php | 2 +- src/Message/ResponseException.php | 2 +- src/Message/ServerRequest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Message/Response.php b/src/Message/Response.php index 45d007ee..429cb120 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -32,7 +32,7 @@ * * @see \Psr\Http\Message\ResponseInterface */ -class Response extends Psr7Response +final class Response extends Psr7Response { /** * @param int $status HTTP status code (e.g. 200/404) diff --git a/src/Message/ResponseException.php b/src/Message/ResponseException.php index 88272242..932290d4 100644 --- a/src/Message/ResponseException.php +++ b/src/Message/ResponseException.php @@ -14,7 +14,7 @@ * The `getCode(): int` method can be used to * return the HTTP response status code. */ -class ResponseException extends RuntimeException +final class ResponseException extends RuntimeException { private $response; diff --git a/src/Message/ServerRequest.php b/src/Message/ServerRequest.php index b0d64498..5c01819b 100644 --- a/src/Message/ServerRequest.php +++ b/src/Message/ServerRequest.php @@ -29,7 +29,7 @@ * * @see ServerRequestInterface */ -class ServerRequest extends Request implements ServerRequestInterface +final class ServerRequest extends Request implements ServerRequestInterface { private $attributes = array(); From 86e0f8003f88d527d71513d4e28f5db77e64fe1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 11 Jul 2020 00:21:02 +0200 Subject: [PATCH 029/152] Update all documentation references to PSR-7 interfaces --- README.md | 127 ++++++++++++++---------------- src/Browser.php | 9 +-- src/Message/Response.php | 7 +- src/Message/ResponseException.php | 2 +- 4 files changed, 68 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index a17b2521..763f025b 100644 --- a/README.md +++ b/README.md @@ -76,8 +76,6 @@ multiple concurrent HTTP requests without blocking. * [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) * [RequestBodyBufferMiddleware](#requestbodybuffermiddleware) * [RequestBodyParserMiddleware](#requestbodyparsermiddleware) - * [ResponseInterface](#responseinterface) - * [RequestInterface](#requestinterface) * [ResponseException](#responseexception) * [Install](#install) * [Tests](#tests) @@ -160,14 +158,16 @@ method. If you want to use any other or even custom HTTP request method, you can use the [`request()`](#request) method. Each of the above methods supports async operation and either *fulfills* with a -[`ResponseInterface`](#responseinterface) or *rejects* with an `Exception`. +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) +or *rejects* with an `Exception`. Please see the following chapter about [promises](#promises) for more details. ### Promises Sending requests is async (non-blocking), so you can actually send multiple requests in parallel. -The `Browser` will respond to each request with a [`ResponseInterface`](#responseinterface) +The `Browser` will respond to each request with a +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) message, the order is not guaranteed. Sending requests uses a [Promise](https://github.com/reactphp/promise)-based interface that makes it easy to react to when an HTTP request is completed @@ -474,11 +474,12 @@ the response body in small chunks as data is received and forwards this data through [ReactPHP's Stream API](https://github.com/reactphp/stream). This works for (any number of) responses of arbitrary sizes. -This means it resolves with a normal [`ResponseInterface`](#responseinterface), +This means it resolves with a normal +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), which can be used to access the response message parameters as usual. You can access the message body as usual, however it now also -implements ReactPHP's [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) -as well as parts of the PSR-7's [`StreamInterface`](https://www.php-fig.org/psr/psr-7/#3-4-psr-http-message-streaminterface). +implements [ReactPHP's `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +as well as parts of the [PSR-7 `StreamInterface`](https://www.php-fig.org/psr/psr-7/#34-psrhttpmessagestreaminterface). ```php $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { @@ -568,7 +569,7 @@ Besides streaming the response body, you can also stream the request body. This can be useful if you want to send big POST requests (uploading files etc.) or process many outgoing streams at once. Instead of passing the body as a string, you can simply pass an instance -implementing ReactPHP's [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +implementing [ReactPHP's `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) to the [request methods](#request-methods) like this: ```php @@ -620,7 +621,7 @@ $connector = new React\Socket\Connector($loop, array( $browser = new React\Http\Browser($loop, $connector); ``` -See also the [HTTP CONNECT proxy example](examples/11-http-connect-proxy.php). +See also the [HTTP CONNECT proxy example](examples/11-client-http-connect-proxy.php). ### SOCKS proxy @@ -647,7 +648,7 @@ $connector = new React\Socket\Connector($loop, array( $browser = new React\Http\Browser($loop, $connector); ``` -See also the [SOCKS proxy example](examples/12-socks-proxy.php). +See also the [SOCKS proxy example](examples/12-client-socks-proxy.php). ### SSH proxy @@ -676,7 +677,7 @@ $connector = new React\Socket\Connector($loop, array( $browser = new React\Http\Browser($loop, $connector); ``` -See also the [SSH proxy example](examples/13-ssh-proxy.php). +See also the [SSH proxy example](examples/13-client-ssh-proxy.php). ### Unix domain sockets @@ -701,7 +702,7 @@ $client->get('http://localhost/info')->then(function (Psr\Http\Message\ResponseI }); ``` -See also the [Unix Domain Sockets (UDS) example](examples/14-unix-domain-sockets.php). +See also the [Unix Domain Sockets (UDS) example](examples/14-client-unix-domain-sockets.php). ## Server Usage @@ -916,9 +917,9 @@ incoming connections and then processing each incoming HTTP request. The request object will be processed once the request has been received by the client. This request object implements the -[PSR-7 ServerRequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#321-psrhttpmessageserverrequestinterface) +[PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface) which in turn extends the -[PSR-7 RequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#32-psrhttpmessagerequestinterface) +[PSR-7 `RequestInterface`](https://www.php-fig.org/psr/psr-7/#32-psrhttpmessagerequestinterface) and will be passed to the callback function like this. ```php @@ -937,9 +938,9 @@ $server = new Server($loop, function (ServerRequestInterface $request) { ``` For more details about the request object, also check out the documentation of -[PSR-7 ServerRequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#321-psrhttpmessageserverrequestinterface) +[PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface) and -[PSR-7 RequestInterface](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#32-psrhttpmessagerequestinterface). +[PSR-7 `RequestInterface`](https://www.php-fig.org/psr/psr-7/#32-psrhttpmessagerequestinterface). #### Request parameters @@ -1149,16 +1150,19 @@ access the request body stream. In the streaming mode, this method returns a stream instance that implements both the [PSR-7 `StreamInterface`](https://www.php-fig.org/psr/psr-7/#34-psrhttpmessagestreaminterface) and the [ReactPHP `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface). -However, most of the PSR-7 `StreamInterface` methods have been -designed under the assumption of being in control of a synchronous request body. +However, most of the +[PSR-7 `StreamInterface`](https://www.php-fig.org/psr/psr-7/#34-psrhttpmessagestreaminterface) +methods have been designed under the assumption of being in control of a +synchronous request body. Given that this does not apply to this server, the following -PSR-7 `StreamInterface` methods are not used and SHOULD NOT be called: +[PSR-7 `StreamInterface`](https://www.php-fig.org/psr/psr-7/#34-psrhttpmessagestreaminterface) +methods are not used and SHOULD NOT be called: `tell()`, `eof()`, `seek()`, `rewind()`, `write()` and `read()`. If this is an issue for your use case and/or you want to access uploaded files, it's highly recommended to use a buffered [request body](#request-body) or use the [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) instead. -The ReactPHP `ReadableStreamInterface` gives you access to the incoming -request body as the individual chunks arrive: +The [ReactPHP `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +gives you access to the incoming request body as the individual chunks arrive: ```php $server = new React\Http\Server( @@ -1223,7 +1227,7 @@ A response message can still be sent (unless the connection is already closed). A `close` event will be emitted after an `error` or `end` event. For more details about the request body stream, check out the documentation of -[ReactPHP ReadableStreamInterface](https://github.com/reactphp/stream#readablestreaminterface). +[ReactPHP `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface). The `getSize(): ?int` method can be used to get the size of the request body, similar to PHP's `$_SERVER['CONTENT_LENGTH']` variable. @@ -1371,13 +1375,14 @@ responsible for processing the request and returning a response, which will be delivered to the client. This function MUST return an instance implementing -[PSR-7 `ResponseInterface`](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#33-psrhttpmessageresponseinterface) +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) object or a [ReactPHP Promise](https://github.com/reactphp/promise) -which resolves with a PSR-7 `ResponseInterface` object. +which resolves with a [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) object. -This projects ships a [`Response` class](#response) which implements the PSR-7 -`ResponseInterface`. In its most simple form, you can use it like this: +This projects ships a [`Response` class](#response) which implements the +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface). +In its most simple form, you can use it like this: ```php $server = new React\Http\Server($loop, function (ServerRequestInterface $request) { @@ -1392,7 +1397,8 @@ $server = new React\Http\Server($loop, function (ServerRequestInterface $request ``` We use this [`Response` class](#response) throughout our project examples, but -feel free to use any other implementation of the PSR-7 `ResponseInterface`. +feel free to use any other implementation of the +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface). See also the [`Response` class](#response) for more details. #### Deferred response @@ -1437,11 +1443,12 @@ If a promise is resolved after the client closes, it will simply be ignored. #### Streaming outgoing response The `Response` class in this project supports to add an instance which implements the -[ReactPHP ReadableStreamInterface](https://github.com/reactphp/stream#readablestreaminterface) +[ReactPHP `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) for the response body. So you are able stream data directly into the response body. -Note that other implementations of the `PSR-7 ResponseInterface` likely -only support strings. +Note that other implementations of the +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) +may only support strings. ```php $server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { @@ -1494,8 +1501,9 @@ in this case (if applicable). writable side of the stream. This can be avoided by either rejecting all requests with the `CONNECT` method (which is what most *normal* origin HTTP servers would likely do) or - or ensuring that only ever an instance of `ReadableStreamInterface` is - used. + or ensuring that only ever an instance of + [ReactPHP's `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) + is used. > > The `101` (Switching Protocols) response code is useful for the more advanced `Upgrade` requests, such as upgrading to the WebSocket protocol or @@ -1711,13 +1719,17 @@ As such, this project supports the concept of middleware request handlers. A middleware request handler is expected to adhere the following rules: * It is a valid `callable`. -* It accepts `ServerRequestInterface` as first argument and an optional - `callable` as second argument. +* It accepts an instance implementing + [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface) + as first argument and an optional `callable` as second argument. * It returns either: - * An instance implementing `ResponseInterface` for direct consumption. + * An instance implementing + [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) + for direct consumption. * Any promise which can be consumed by [`Promise\resolve()`](https://reactphp.org/promise/#resolve) resolving to a - `ResponseInterface` for deferred consumption. + [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) + for deferred consumption. * It MAY throw an `Exception` (or return a rejected promise) in order to signal an error condition and abort the chain. * It calls `$next($request)` to continue processing the next middleware @@ -1772,8 +1784,8 @@ $server = new Server( Similarly, you can use the result of the `$next` middleware request handler function to modify the outgoing response. Note that as per the above documentation, the `$next` middleware request handler may return a -`ResponseInterface` directly or one wrapped in a promise for deferred -resolution. +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) +directly or one wrapped in a promise for deferred resolution. In order to simplify handling both paths, you can simply wrap this in a [`Promise\resolve()`](https://reactphp.org/promise/#resolve) call like this: @@ -1840,8 +1852,9 @@ encourages third-party middleware implementations. While we would love to support PSR-15 directly in `react/http`, we understand that this interface does not specifically target async APIs and as such does not take advantage of promises for [deferred responses](#deferred-response). -The gist of this is that where PSR-15 enforces a `ResponseInterface` return -value, we also accept a `PromiseInterface`. +The gist of this is that where PSR-15 enforces a +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) +return value, we also accept a `PromiseInterface`. As such, we suggest using the external [PSR-15 middleware adapter](https://github.com/friends-of-reactphp/http-middleware-psr15-adapter) that uses on the fly monkey patching of these return values which makes using @@ -2116,7 +2129,7 @@ $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\Respons }); ``` -See also [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +See also [ReactPHP's `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) and the [streaming response](#streaming-response) for more details, examples and possible use-cases. @@ -2381,10 +2394,9 @@ This class implements the which in turn extends the [PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). -> Internally, this class extends the underlying `\RingCentral\Psr7\Response` - class. The only difference is that this class will accept implemenations - of ReactPHPs `ReadableStreamInterface` for the `$body` argument. This base - class is considered an implementation detail that may change in the future. +> Internally, this implementation builds on top of an existing incoming + response message and only adds required streaming support. This base class is + considered an implementation detail that may change in the future. #### ServerRequest @@ -2593,7 +2605,8 @@ Instead of relying on these superglobals, you can use the `$request->getParsedBody()` and `$request->getUploadedFiles()` methods as defined by PSR-7. -Accordingly, each file upload will be represented as instance implementing [`UploadedFileInterface`](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md#36-psrhttpmessageuploadedfileinterface). +Accordingly, each file upload will be represented as instance implementing the +[PSR-7 `UploadedFileInterface`](https://www.php-fig.org/psr/psr-7/#36-psrhttpmessageuploadedfileinterface). Due to its blocking nature, the `moveTo()` method is not available and throws a `RuntimeException` instead. You can use `$contents = (string)$file->getStream();` to access the file @@ -2697,26 +2710,6 @@ new RequestBodyParserMiddleware(10 * 1024, 100); // 100 files with 10 KiB each If you want to respect this setting, you have to check its value and effectively avoid using this middleware entirely. -### ResponseInterface - -The `Psr\Http\Message\ResponseInterface` represents the incoming response received from the [`Browser`](#browser). - -This is a standard interface defined in -[PSR-7: HTTP message interfaces](https://www.php-fig.org/psr/psr-7/), see its -[`ResponseInterface` definition](https://www.php-fig.org/psr/psr-7/#3-3-psr-http-message-responseinterface) -which in turn extends the -[`MessageInterface` definition](https://www.php-fig.org/psr/psr-7/#3-1-psr-http-message-messageinterface). - -### RequestInterface - -The `Psr\Http\Message\RequestInterface` represents the outgoing request to be sent via the [`Browser`](#browser). - -This is a standard interface defined in -[PSR-7: HTTP message interfaces](https://www.php-fig.org/psr/psr-7/), see its -[`RequestInterface` definition](https://www.php-fig.org/psr/psr-7/#3-2-psr-http-message-requestinterface) -which in turn extends the -[`MessageInterface` definition](https://www.php-fig.org/psr/psr-7/#3-1-psr-http-message-messageinterface). - ### ResponseException The `ResponseException` is an `Exception` sub-class that will be used to reject @@ -2728,7 +2721,7 @@ The `getCode(): int` method can be used to return the HTTP response status code. The `getResponse(): ResponseInterface` method can be used to -access its underlying [`ResponseInterface`](#responseinterface) object. +access its underlying response object. ## Install diff --git a/src/Browser.php b/src/Browser.php index 9da950ba..d222ac38 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -2,16 +2,15 @@ namespace React\Http; +use Psr\Http\Message\ResponseInterface; +use React\EventLoop\LoopInterface; use React\Http\Io\Sender; use React\Http\Io\Transaction; use React\Http\Message\MessageFactory; -use InvalidArgumentException; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\UriInterface; -use React\EventLoop\LoopInterface; use React\Promise\PromiseInterface; use React\Socket\ConnectorInterface; use React\Stream\ReadableStreamInterface; +use InvalidArgumentException; /** * @final This class is final and shouldn't be extended as it is likely to be marked final in a future relase. @@ -719,7 +718,7 @@ private function withOptions(array $options) /** * @param string $method - * @param string|UriInterface $url + * @param string $url * @param array $headers * @param string|ReadableStreamInterface $contents * @return PromiseInterface diff --git a/src/Message/Response.php b/src/Message/Response.php index 429cb120..5d799c58 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -25,10 +25,9 @@ * which in turn extends the * [PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). * - * > Internally, this class extends the underlying `\RingCentral\Psr7\Response` - * class. The only difference is that this class will accept implemenations - * of ReactPHPs `ReadableStreamInterface` for the `$body` argument. This base - * class is considered an implementation detail that may change in the future. + * > Internally, this implementation builds on top of an existing incoming + * response message and only adds required streaming support. This base class is + * considered an implementation detail that may change in the future. * * @see \Psr\Http\Message\ResponseInterface */ diff --git a/src/Message/ResponseException.php b/src/Message/ResponseException.php index 932290d4..ab488a1b 100644 --- a/src/Message/ResponseException.php +++ b/src/Message/ResponseException.php @@ -32,7 +32,7 @@ public function __construct(ResponseInterface $response, $message = null, $code } /** - * Access its underlying [`ResponseInterface`](#responseinterface) object. + * Access its underlying response object. * * @return ResponseInterface */ From cc94133d4c3156b0442c963c093814c456415ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 11 Jul 2020 00:35:27 +0200 Subject: [PATCH 030/152] Use fully-qualified class names throughout the documentation --- README.md | 148 +++++++++--------- src/Message/ResponseException.php | 2 +- .../LimitConcurrentRequestsMiddleware.php | 28 ++-- src/Server.php | 6 +- 4 files changed, 92 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 763f025b..1e405917 100644 --- a/README.md +++ b/README.md @@ -709,7 +709,7 @@ See also the [Unix Domain Sockets (UDS) example](examples/14-client-unix-domain- ### Server -The `Server` class is responsible for handling incoming connections and then +The `React\Http\Server` class is responsible for handling incoming connections and then processing each incoming HTTP request. When a complete HTTP request has been received, it will invoke the given @@ -923,11 +923,11 @@ which in turn extends the and will be passed to the callback function like this. ```php -$server = new Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { $body = "The method of the request is: " . $request->getMethod(); $body .= "The requested path is: " . $request->getUri()->getPath(); - return new Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -966,10 +966,10 @@ The following parameters are currently available: Set to 'on' if the request used HTTPS, otherwise it won't be set ```php -$server = new Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; - return new Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -991,7 +991,7 @@ The `getQueryParams(): array` method can be used to get the query parameters similiar to the `$_GET` variable. ```php -$server = new Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { $queryParams = $request->getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; @@ -1001,7 +1001,7 @@ $server = new Server($loop, function (ServerRequestInterface $request) { $body = 'The value of "foo" is: ' . htmlspecialchars($queryParams['foo']); } - return new Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/html' @@ -1045,10 +1045,10 @@ By default, this method will only return parsed data for requests using request headers (commonly used for `POST` requests for HTML form submission data). ```php -$server = new Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { $name = $request->getParsedBody()['name'] ?? 'anonymous'; - return new Response( + return new React\Http\Message\Response( 200, array(), "Hello $name!\n" @@ -1069,11 +1069,11 @@ an XML (`Content-Type: application/xml`) request body (which is commonly used fo `POST`, `PUT` or `PATCH` requests in JSON-based or RESTful/RESTish APIs). ```php -$server = new Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { $data = json_decode((string)$request->getBody()); $name = $data->name ?? 'anonymous'; - return new Response( + return new React\Http\Message\Response( 200, array('Content-Type' => 'application/json'), json_encode(['message' => "Hello $name!"]) @@ -1092,11 +1092,11 @@ This array will only be filled when using the `Content-Type: multipart/form-data request header (commonly used for `POST` requests for HTML file uploads). ```php -$server = new Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { $files = $request->getUploadedFiles(); $name = isset($files['avatar']) ? $files['avatar']->getClientFilename() : 'nothing'; - return new Response( + return new React\Http\Message\Response( 200, array(), "Uploaded $name\n" @@ -1313,13 +1313,13 @@ The `getCookieParams(): string[]` method can be used to get all cookies sent with the current request. ```php -$server = new Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { $key = 'react\php'; if (isset($request->getCookieParams()[$key])) { $body = "Your cookie value is: " . $request->getCookieParams()[$key]; - return new Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -1328,7 +1328,7 @@ $server = new Server($loop, function (ServerRequestInterface $request) { ); } - return new Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain', @@ -1385,7 +1385,7 @@ This projects ships a [`Response` class](#response) which implements the In its most simple form, you can use it like this: ```php -$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1413,10 +1413,10 @@ To prevent this you SHOULD use a This example shows how such a long-term action could look like: ```php -$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) use ($loop) { return new Promise(function ($resolve, $reject) use ($loop) { $loop->addTimer(1.5, function() use ($resolve) { - $response = new Response( + $response = new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -1451,7 +1451,7 @@ Note that other implementations of the may only support strings. ```php -$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) use ($loop) { $stream = new ThroughStream(); $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { @@ -1463,7 +1463,7 @@ $server = new Server($loop, function (ServerRequestInterface $request) use ($loo $stream->end(); }); - return new Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -1543,8 +1543,8 @@ added automatically. This is the most common use case, for example when using a `string` response body like this: ```php -$server = new Server($loop, function (ServerRequestInterface $request) { - return new Response( +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -1562,14 +1562,14 @@ response messages will contain the plain response body. If you know the length of your streaming response body, you MAY want to specify it explicitly like this: ```php -$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { +$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(2.0, function () use ($stream) { $stream->end("Hello World!\n"); }); - return new Response( + return new React\Http\Message\Response( 200, array( 'Content-Length' => '13', @@ -1765,14 +1765,14 @@ The following example adds a middleware request handler that adds the current ti header (`Request-Time`) and a final request handler that always returns a 200 code without a body: ```php -$server = new Server( +$server = new React\Http\Server( $loop, - function (ServerRequestInterface $request, callable $next) { + function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { $request = $request->withHeader('Request-Time', time()); return $next($request); }, - function (ServerRequestInterface $request) { - return new Response(200); + function (Psr\Http\Message\ServerRequestInterface $request) { + return new React\Http\Message\Response(200); } ); ``` @@ -1790,16 +1790,16 @@ In order to simplify handling both paths, you can simply wrap this in a [`Promise\resolve()`](https://reactphp.org/promise/#resolve) call like this: ```php -$server = new Server( +$server = new React\Http\Server( $loop, - function (ServerRequestInterface $request, callable $next) { + function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { $promise = React\Promise\resolve($next($request)); return $promise->then(function (ResponseInterface $response) { return $response->withHeader('Content-Type', 'text/html'); }); }, - function (ServerRequestInterface $request) { - return new Response(200); + function (Psr\Http\Message\ServerRequestInterface $request) { + return new React\Http\Message\Response(200); } ); ``` @@ -1813,25 +1813,25 @@ handling logic (or logging etc.) by wrapping this in a [`Promise`](https://reactphp.org/promise/#promise) like this: ```php -$server = new Server( +$server = new React\Http\Server( $loop, - function (ServerRequestInterface $request, callable $next) { + function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { $promise = new React\Promise\Promise(function ($resolve) use ($next, $request) { $resolve($next($request)); }); return $promise->then(null, function (Exception $e) { - return new Response( + return new React\Http\Message\Response( 500, array(), 'Internal error: ' . $e->getMessage() ); }); }, - function (ServerRequestInterface $request) { + function (Psr\Http\Message\ServerRequestInterface $request) { if (mt_rand(0, 1) === 1) { throw new RuntimeException('Database error'); } - return new Response(200); + return new React\Http\Message\Response(200); } ); ``` @@ -2376,7 +2376,7 @@ given setting applied. #### Response -The `Response` class can be used to +The `React\Http\Message\Response` class can be used to represent an outgoing server response message. ```php @@ -2400,7 +2400,7 @@ which in turn extends the #### ServerRequest -The `ServerRequest` class can be used to +The `React\Http\Message\ServerRequest` class can be used to respresent an incoming server request message. This class implements the @@ -2422,7 +2422,7 @@ application reacts to certain HTTP requests. #### StreamingRequestMiddleware -The `StreamingRequestMiddleware` can be used to +The `React\Http\Middleware\StreamingRequestMiddleware` can be used to process incoming requests with a streaming request body (without buffering). This allows you to process requests of any size without buffering the request @@ -2480,7 +2480,7 @@ $server = new React\Http\Server(array( #### LimitConcurrentRequestsMiddleware -The `LimitConcurrentRequestsMiddleware` can be used to +The `React\Http\Middleware\LimitConcurrentRequestsMiddleware` can be used to limit how many next handlers can be executed concurrently. If this middleware is invoked, it will check if the number of pending @@ -2498,9 +2498,9 @@ The following example shows how this middleware can be used to ensure no more than 10 handlers will be invoked at once: ```php -$server = new Server( +$server = new React\Http\Server( $loop, - new LimitConcurrentRequestsMiddleware(10), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(10), $handler ); ``` @@ -2510,12 +2510,12 @@ Similarly, this middleware is often used in combination with the to limit the total number of requests that can be buffered at once: ```php -$server = new Server( +$server = new React\Http\Server( $loop, - new StreamingRequestMiddleware(), - new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers - new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request - new RequestBodyParserMiddleware(), + new React\Http\Middleware\StreamingRequestMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + new React\Http\Middleware\RequestBodyParserMiddleware(), $handler ); ``` @@ -2525,20 +2525,20 @@ that can be buffered at once and then ensure the actual request handler only processes one request after another without any concurrency: ```php -$server = new Server( +$server = new React\Http\Server( $loop, - new StreamingRequestMiddleware(), - new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers - new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request - new RequestBodyParserMiddleware(), - new LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency) + new React\Http\Middleware\StreamingRequestMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + new React\Http\Middleware\RequestBodyParserMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency) $handler ); ``` #### RequestBodyBufferMiddleware -One of the built-in middleware is the `RequestBodyBufferMiddleware` which +One of the built-in middleware is the `React\Http\Middleware\RequestBodyBufferMiddleware` which can be used to buffer the whole incoming request body in memory. This can be useful if full PSR-7 compatibility is needed for the request handler and the default streaming request body handling is not needed. @@ -2579,21 +2579,21 @@ the total number of concurrent requests. Usage: ```php -$server = new Server( +$server = new React\Http\Server( $loop, - new StreamingRequestMiddleware(), - new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers - new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB - function (ServerRequestInterface $request) { + new React\Http\Middleware\StreamingRequestMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + new React\Http\Middleware\RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB + function (Psr\Http\Message\ServerRequestInterface $request) { // The body from $request->getBody() is now fully available without the need to stream it - return new Response(200); + return new React\Http\Message\Response(200); }, ); ``` #### RequestBodyParserMiddleware -The `RequestBodyParserMiddleware` takes a fully buffered request body +The `React\Http\Middleware\RequestBodyParserMiddleware` takes a fully buffered request body (generally from [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware)), and parses the form values and file uploads from the incoming HTTP request body. @@ -2613,14 +2613,14 @@ You can use `$contents = (string)$file->getStream();` to access the file contents and persist this to your favorite data store. ```php -$handler = function (ServerRequestInterface $request) { +$handler = function (Psr\Http\Message\ServerRequestInterface $request) { // If any, parsed form fields are now available from $request->getParsedBody() $body = $request->getParsedBody(); $name = isset($body['name']) ? $body['name'] : 'unnamed'; $files = $request->getUploadedFiles(); $avatar = isset($files['avatar']) ? $files['avatar'] : null; - if ($avatar instanceof UploadedFileInterface) { + if ($avatar instanceof Psr\Http\Message\UploadedFileInterface) { if ($avatar->getError() === UPLOAD_ERR_OK) { $uploaded = $avatar->getSize() . ' bytes'; } elseif ($avatar->getError() === UPLOAD_ERR_INI_SIZE) { @@ -2632,7 +2632,7 @@ $handler = function (ServerRequestInterface $request) { $uploaded = 'nothing'; } - return new Response( + return new React\Http\Message\Response( 200, array( 'Content-Type' => 'text/plain' @@ -2641,12 +2641,12 @@ $handler = function (ServerRequestInterface $request) { ); }; -$server = new Server( +$server = new React\Http\Server( $loop, - new StreamingRequestMiddleware(), - new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers - new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB - new RequestBodyParserMiddleware(), + new React\Http\Middleware\StreamingRequestMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + new React\Http\Middleware\RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB + new React\Http\Middleware\RequestBodyParserMiddleware(), $handler ); ``` @@ -2662,7 +2662,7 @@ explicitly passing the maximum filesize in bytes as the first parameter to the constructor like this: ```php -new RequestBodyParserMiddleware(8 * 1024 * 1024); // 8 MiB limit per file +new React\Http\Middleware\RequestBodyParserMiddleware(8 * 1024 * 1024); // 8 MiB limit per file ``` By default, this middleware respects the @@ -2678,7 +2678,7 @@ You can control the maximum number of file uploads per request by explicitly passing the second parameter to the constructor like this: ```php -new RequestBodyParserMiddleware(10 * 1024, 100); // 100 files with 10 KiB each +new React\Http\Middleware\RequestBodyParserMiddleware(10 * 1024, 100); // 100 files with 10 KiB each ``` > Note that this middleware handler simply parses everything that is already @@ -2712,7 +2712,7 @@ new RequestBodyParserMiddleware(10 * 1024, 100); // 100 files with 10 KiB each ### ResponseException -The `ResponseException` is an `Exception` sub-class that will be used to reject +The `React\Http\Message\ResponseException` is an `Exception` sub-class that will be used to reject a request promise if the remote server returns a non-success status code (anything but 2xx or 3xx). You can control this behavior via the [`withRejectErrorResponse()` method](#withrejecterrorresponse). diff --git a/src/Message/ResponseException.php b/src/Message/ResponseException.php index ab488a1b..f4912c90 100644 --- a/src/Message/ResponseException.php +++ b/src/Message/ResponseException.php @@ -6,7 +6,7 @@ use Psr\Http\Message\ResponseInterface; /** - * The `ResponseException` is an `Exception` sub-class that will be used to reject + * The `React\Http\Message\ResponseException` is an `Exception` sub-class that will be used to reject * a request promise if the remote server returns a non-success status code * (anything but 2xx or 3xx). * You can control this behavior via the [`withRejectErrorResponse()` method](#withrejecterrorresponse). diff --git a/src/Middleware/LimitConcurrentRequestsMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php index d6760e95..9aaf5ff2 100644 --- a/src/Middleware/LimitConcurrentRequestsMiddleware.php +++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -29,10 +29,10 @@ * than 10 handlers will be invoked at once: * * ```php - * $server = new Server( + * $server = new React\Http\Server( * $loop, - * new StreamingRequestMiddleware(), - * new LimitConcurrentRequestsMiddleware(10), + * new React\Http\Middleware\StreamingRequestMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(10), * $handler * ); * ``` @@ -42,12 +42,12 @@ * to limit the total number of requests that can be buffered at once: * * ```php - * $server = new Server( + * $server = new React\Http\Server( * $loop, - * new StreamingRequestMiddleware(), - * new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers - * new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request - * new RequestBodyParserMiddleware(), + * new React\Http\Middleware\StreamingRequestMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + * new React\Http\Middleware\RequestBodyParserMiddleware(), * $handler * ); * ``` @@ -57,13 +57,13 @@ * processes one request after another without any concurrency: * * ```php - * $server = new Server( + * $server = new React\Http\Server( * $loop, - * new StreamingRequestMiddleware(), - * new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers - * new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request - * new RequestBodyParserMiddleware(), - * new LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency) + * new React\Http\Middleware\StreamingRequestMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + * new React\Http\Middleware\RequestBodyParserMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency) * $handler * ); * ``` diff --git a/src/Server.php b/src/Server.php index 3b5d70be..1aa5d405 100644 --- a/src/Server.php +++ b/src/Server.php @@ -14,7 +14,7 @@ use React\Socket\ServerInterface; /** - * The `Server` class is responsible for handling incoming connections and then + * The `React\Http\Server` class is responsible for handling incoming connections and then * processing each incoming HTTP request. * * When a complete HTTP request has been received, it will invoke the given @@ -292,9 +292,9 @@ public function __construct(LoopInterface $loop) * * @param ServerInterface $socket */ - public function listen(ServerInterface $server) + public function listen(ServerInterface $socket) { - $this->streamingServer->listen($server); + $this->streamingServer->listen($socket); } /** From 552dbdd477ac894be141eb83c58f972cadde6e44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 11 Jul 2020 00:47:11 +0200 Subject: [PATCH 031/152] Streaming `Message` documentation and move internal message classes --- README.md | 28 +++++++------- src/Browser.php | 2 +- src/{Message => Io}/MessageFactory.php | 2 +- src/{Message => Io}/ReadableBodyStream.php | 2 +- src/Io/Sender.php | 1 - src/Io/Transaction.php | 3 +- tests/{Message => Io}/MessageFactoryTest.php | 4 +- .../ReadableBodyStreamTest.php | 4 +- tests/Io/SenderTest.php | 38 +++++++++---------- tests/Io/TransactionTest.php | 8 ++-- 10 files changed, 45 insertions(+), 47 deletions(-) rename src/{Message => Io}/MessageFactory.php (99%) rename src/{Message => Io}/ReadableBodyStream.php (99%) rename tests/{Message => Io}/MessageFactoryTest.php (99%) rename tests/{Message => Io}/ReadableBodyStreamTest.php (98%) diff --git a/README.md b/README.md index 1e405917..4a72d9e7 100644 --- a/README.md +++ b/README.md @@ -71,12 +71,12 @@ multiple concurrent HTTP requests without blocking. * [React\Http\Message](#reacthttpmessage) * [Response](#response) * [ServerRequest](#serverrequest) + * [ResponseException](#responseexception) * [React\Http\Middleware](#reacthttpmiddleware) * [StreamingRequestMiddleware](#streamingrequestmiddleware) * [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) * [RequestBodyBufferMiddleware](#requestbodybuffermiddleware) * [RequestBodyParserMiddleware](#requestbodyparsermiddleware) - * [ResponseException](#responseexception) * [Install](#install) * [Tests](#tests) * [License](#license) @@ -2418,6 +2418,19 @@ application reacts to certain HTTP requests. request message and only adds required server methods. This base class is considered an implementation detail that may change in the future. +#### ResponseException + +The `React\Http\Message\ResponseException` is an `Exception` sub-class that will be used to reject +a request promise if the remote server returns a non-success status code +(anything but 2xx or 3xx). +You can control this behavior via the [`withRejectErrorResponse()` method](#withrejecterrorresponse). + +The `getCode(): int` method can be used to +return the HTTP response status code. + +The `getResponse(): ResponseInterface` method can be used to +access its underlying response object. + ### React\Http\Middleware #### StreamingRequestMiddleware @@ -2710,19 +2723,6 @@ new React\Http\Middleware\RequestBodyParserMiddleware(10 * 1024, 100); // 100 fi If you want to respect this setting, you have to check its value and effectively avoid using this middleware entirely. -### ResponseException - -The `React\Http\Message\ResponseException` is an `Exception` sub-class that will be used to reject -a request promise if the remote server returns a non-success status code -(anything but 2xx or 3xx). -You can control this behavior via the [`withRejectErrorResponse()` method](#withrejecterrorresponse). - -The `getCode(): int` method can be used to -return the HTTP response status code. - -The `getResponse(): ResponseInterface` method can be used to -access its underlying response object. - ## Install The recommended way to install this library is [through Composer](https://getcomposer.org). diff --git a/src/Browser.php b/src/Browser.php index d222ac38..46a5d115 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -4,9 +4,9 @@ use Psr\Http\Message\ResponseInterface; use React\EventLoop\LoopInterface; +use React\Http\Io\MessageFactory; use React\Http\Io\Sender; use React\Http\Io\Transaction; -use React\Http\Message\MessageFactory; use React\Promise\PromiseInterface; use React\Socket\ConnectorInterface; use React\Stream\ReadableStreamInterface; diff --git a/src/Message/MessageFactory.php b/src/Io/MessageFactory.php similarity index 99% rename from src/Message/MessageFactory.php rename to src/Io/MessageFactory.php index eaa144cd..14591e80 100644 --- a/src/Message/MessageFactory.php +++ b/src/Io/MessageFactory.php @@ -1,6 +1,6 @@ loop, null, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = Sender::createFromLoop($this->loop, null, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $this->assertInstanceOf('React\Http\Io\Sender', $sender); } @@ -36,7 +36,7 @@ public function testSenderRejectsInvalidUri() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->never())->method('connect'); - $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $request = new Request('GET', 'www.google.com'); @@ -51,7 +51,7 @@ 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), $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $request = new Request('GET', 'http://www.google.com/'); @@ -71,7 +71,7 @@ public function testSendPostWillAutomaticallySendContentLengthHeader() '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $request = new Request('POST', 'http://www.google.com/', array(), 'hello'); $sender->send($request); @@ -87,7 +87,7 @@ public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmpty '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $request = new Request('POST', 'http://www.google.com/', array(), ''); $sender->send($request); @@ -106,7 +106,7 @@ public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() '1.1' )->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -122,7 +122,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAn $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -142,7 +142,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -162,7 +162,7 @@ public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $expected = new \RuntimeException(); $stream = new ThroughStream(); @@ -192,7 +192,7 @@ public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -220,7 +220,7 @@ public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -247,7 +247,7 @@ public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array('Content-Length' => '100'), new ReadableBodyStream($stream)); @@ -264,7 +264,7 @@ public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $request = new Request('GET', 'http://www.google.com/'); $sender->send($request); @@ -280,7 +280,7 @@ public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyReques '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $request = new Request('CUSTOM', 'http://www.google.com/'); $sender->send($request); @@ -296,7 +296,7 @@ public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsI '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $request = new Request('CUSTOM', 'http://www.google.com/', array('Content-Length' => '0')); $sender->send($request); @@ -311,7 +311,7 @@ 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), $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $request = new Request('GET', 'http://www.google.com/'); @@ -330,7 +330,7 @@ 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), $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $request = new Request('GET', 'http://www.google.com/'); @@ -387,7 +387,7 @@ public function testRequestProtocolVersion(Request $Request, $method, $uri, $hea $http->expects($this->once())->method('request')->with($method, $uri, $headers, $protocolVersion)->willReturn($request); - $sender = new Sender($http, $this->getMockBuilder('React\Http\Message\MessageFactory')->getMock()); + $sender = new Sender($http, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); $sender->send($Request); } } diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 882b1860..88320bf0 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -3,16 +3,16 @@ namespace React\Tests\Http\Io; use Clue\React\Block; -use React\Http\Io\Transaction; -use React\Http\Message\MessageFactory; -use React\Http\Message\ResponseException; -use React\Tests\Http\TestCase; use PHPUnit\Framework\MockObject\MockObject; use Psr\Http\Message\RequestInterface; +use React\Http\Io\MessageFactory; +use React\Http\Io\Transaction; +use React\Http\Message\ResponseException; use React\EventLoop\Factory; use React\Promise; use React\Promise\Deferred; use React\Stream\ThroughStream; +use React\Tests\Http\TestCase; use RingCentral\Psr7\Response; class TransactionTest extends TestCase From 865694453c95122f8972b9ed7961efb3c517fc5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 11 Jul 2020 15:29:43 +0200 Subject: [PATCH 032/152] Prepare v1.0.0 release --- CHANGELOG.md | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 3 +- 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 465356cc..aadbfa7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,104 @@ # Changelog +## 1.0.0 (2020-07-11) + +A major new feature release, see [**release announcement**](https://clue.engineering/2020/announcing-reactphp-http). + +* First stable LTS release, now following [SemVer](https://semver.org/). + We'd like to emphasize that this component is production ready and battle-tested. + We plan to support all long-term support (LTS) releases for at least 24 months, + so you have a rock-solid foundation to build on top of. + +This update involves some major new features and a number of BC breaks due to +some necessary API cleanup. We've tried hard to avoid BC breaks where possible +and minimize impact otherwise. We expect that most consumers of this package +will be affected by BC breaks, but updating should take no longer than a few +minutes. See below for more details: + +* Feature: Add async HTTP client implementation. + (#368 by @clue) + + ```php + $browser = new React\Http\Browser($loop); + $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + echo $response->getBody(); + }); + ``` + + The code has been imported as-is from [clue/reactphp-buzz v2.9.0](https://github.com/clue/reactphp-buzz), + with only minor changes to the namespace and we otherwise leave all the existing APIs unchanged. + Upgrading from [clue/reactphp-buzz v2.9.0](https://github.com/clue/reactphp-buzz) + to this release should be a matter of updating some namespace references only: + + ```php + // old + $browser = new Clue\React\Buzz\Browser($loop); + + // new + $browser = new React\Http\Browser($loop); + ``` + +* Feature / BC break: Add `LoopInterface` as required first constructor argument to `Server` and + change `Server` to accept variadic middleware handlers instead of `array`. + (#361 and #362 by @WyriHaximus) + + ```php + // old + $server = new React\Http\Server($handler); + $server = new React\Http\Server([$middleware, $handler]); + + // new + $server = new React\Http\Server($loop, $handler); + $server = new React\Http\Server($loop, $middleware, $handler); + ``` + +* Feature / BC break: Move `Response` class to `React\Http\Message\Response` and + expose `ServerRequest` class to `React\Http\Message\ServerRequest`. + (#370 by @clue) + + ```php + // old + $response = new React\Http\Response(200, [], 'Hello!'); + + // new + $response = new React\Http\Message\Response(200, [], 'Hello!'); + ``` + +* Feature / BC break: Add `StreamingRequestMiddleware` to stream incoming requests, mark `StreamingServer` as internal. + (#367 by @clue) + + ```php + // old: advanced StreamingServer is now internal only + $server = new React\Http\StreamingServer($handler); + + // new: use StreamingRequestMiddleware instead of StreamingServer + $server = new React\Http\Server( + $loop, + new React\Http\Middleware\StreamingRequestMiddleware(), + $handler + ); + ``` + +* Feature / BC break: Improve default concurrency to 1024 requests and cap default request buffer at 64K. + (#371 by @clue) + + This improves default concurrency to 1024 requests and caps the default request buffer at 64K. + The previous defaults resulted in just 4 concurrent requests with a request buffer of 8M. + See [`Server`](../README.md#server) for details on how to override these defaults. + +* Feature: Expose ReactPHP in `User-Agent` client-side request header and in `Server` server-side response header. + (#374 by @clue) + +* Mark all classes as `final` to discourage inheriting from it. + (#373 by @WyriHaximus) + +* Improve documentation and use fully-qualified class names throughout the documentation and + add ReactPHP core team as authors to `composer.json` and license file. + (#366 and #369 by @WyriHaximus and #375 by @clue) + +* Improve test suite and support skipping all online tests with `--exclude-group internet`. + (#372 by @clue) + ## 0.8.7 (2020-07-05) * Fix: Fix parsing multipart request body with quoted header parameters (dot net). diff --git a/README.md b/README.md index 4a72d9e7..54fd9196 100644 --- a/README.md +++ b/README.md @@ -2728,10 +2728,11 @@ new React\Http\Middleware\RequestBodyParserMiddleware(10 * 1024, 100); // 100 fi The recommended way to install this library is [through Composer](https://getcomposer.org). [New to Composer?](https://getcomposer.org/doc/00-intro.md) +This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require react/http:^0.8.7 +$ composer require react/http:^1.0 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 0daf98f59f6873a9778e70a30f39d9fce24ca7c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 Jul 2020 01:30:21 +0200 Subject: [PATCH 033/152] Consistently resolve base URL according to HTTP specs --- README.md | 14 +++++++------- src/Browser.php | 20 +++++++++++--------- src/Io/MessageFactory.php | 37 ------------------------------------- tests/BrowserTest.php | 31 +++++++++++++------------------ 4 files changed, 31 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 54fd9196..78709bba 100644 --- a/README.md +++ b/README.md @@ -2282,16 +2282,16 @@ The `withBase(string|null $baseUrl): Browser` method can be used to change the base URL used to resolve relative URLs to. If you configure a base URL, any requests to relative URLs will be -processed by first prepending this absolute base URL. Note that this -merely prepends the base URL and does *not* resolve any relative path -references (like `../` etc.). This is mostly useful for (RESTful) API -calls where all endpoints (URLs) are located under a common base URL. +processed by first resolving this relative to the given absolute base +URL. This supports resolving relative path references (like `../` etc.). +This is particularly useful for (RESTful) API calls where all endpoints +(URLs) are located under a common base URL. ```php -$browser = $browser->withBase('http://api.example.com/v3'); +$browser = $browser->withBase('http://api.example.com/v3/'); -// will request http://api.example.com/v3/example -$browser->get('/example')->then(…); +// will request http://api.example.com/v3/users +$browser->get('users')->then(…); ``` You can pass in a `null` base URL to return a new instance that does not diff --git a/src/Browser.php b/src/Browser.php index 46a5d115..622404bf 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -3,6 +3,7 @@ namespace React\Http; use Psr\Http\Message\ResponseInterface; +use RingCentral\Psr7\Uri; use React\EventLoop\LoopInterface; use React\Http\Io\MessageFactory; use React\Http\Io\Sender; @@ -542,16 +543,16 @@ public function withRejectErrorResponse($obeySuccessCode) * Changes the base URL used to resolve relative URLs to. * * If you configure a base URL, any requests to relative URLs will be - * processed by first prepending this absolute base URL. Note that this - * merely prepends the base URL and does *not* resolve any relative path - * references (like `../` etc.). This is mostly useful for (RESTful) API - * calls where all endpoints (URLs) are located under a common base URL. + * processed by first resolving this relative to the given absolute base + * URL. This supports resolving relative path references (like `../` etc.). + * This is particularly useful for (RESTful) API calls where all endpoints + * (URLs) are located under a common base URL. * * ```php - * $browser = $browser->withBase('http://api.example.com/v3'); + * $browser = $browser->withBase('http://api.example.com/v3/'); * - * // will request http://api.example.com/v3/example - * $browser->get('/example')->then(…); + * // will request http://api.example.com/v3/users + * $browser->get('users')->then(…); * ``` * * You can pass in a `null` base URL to return a new instance that does not @@ -725,12 +726,13 @@ private function withOptions(array $options) */ private function requestMayBeStreaming($method, $url, array $headers = array(), $contents = '') { - $request = $this->messageFactory->request($method, $url, $headers, $contents, $this->protocolVersion); if ($this->baseUrl !== null) { // ensure we're actually below the base URL - $request = $request->withUri($this->messageFactory->expandBase($request->getUri(), $this->baseUrl)); + $url = Uri::resolve($this->baseUrl, $url); } + $request = $this->messageFactory->request($method, $url, $headers, $contents, $this->protocolVersion); + return $this->transaction->send($request); } } diff --git a/src/Io/MessageFactory.php b/src/Io/MessageFactory.php index 14591e80..5b7a2b35 100644 --- a/src/Io/MessageFactory.php +++ b/src/Io/MessageFactory.php @@ -99,41 +99,4 @@ public function uriRelative(UriInterface $base, $uri) { return Uri::resolve($base, $uri); } - - /** - * Resolves the given relative or absolute $uri by appending it behind $this base URI - * - * The given $uri parameter can be either a relative or absolute URI and - * as such can not contain any URI template placeholders. - * - * As such, the outcome of this method represents a valid, absolute URI - * which will be returned as an instance implementing `UriInterface`. - * - * If the given $uri is a relative URI, it will simply be appended behind $base URI. - * - * If the given $uri is an absolute URI, it will simply be returned as-is. - * - * @param UriInterface $uri - * @param UriInterface $base - * @return UriInterface - */ - public function expandBase(UriInterface $uri, UriInterface $base) - { - if ($uri->getScheme() !== '') { - return $uri; - } - - $uri = (string)$uri; - $base = (string)$base; - - if ($uri !== '' && substr($base, -1) !== '/' && substr($uri, 0, 1) !== '?') { - $base .= '/'; - } - - if (isset($uri[0]) && $uri[0] === '/') { - $uri = substr($uri, 1); - } - - return $this->uri($base . $uri); - } } diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index 88ef107e..612875fc 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -223,35 +223,30 @@ public function provideOtherUris() 'http://example.com/base/another', 'http://example.com/base/another', ), - 'slash returns added slash' => array( + 'slash returns base without path' => array( 'http://example.com/base', '/', - 'http://example.com/base/', - ), - 'slash does not add duplicate slash if base already ends with slash' => array( - 'http://example.com/base/', - '/', - 'http://example.com/base/', + 'http://example.com/', ), 'relative is added behind base' => array( 'http://example.com/base/', 'test', 'http://example.com/base/test', ), - 'relative with slash is added behind base without duplicate slashes' => array( - 'http://example.com/base/', - '/test', - 'http://example.com/base/test', - ), - 'relative is added behind base with automatic slash inbetween' => array( + 'relative is added behind base without path' => array( 'http://example.com/base', 'test', - 'http://example.com/base/test', + 'http://example.com/test', ), - 'relative with slash is added behind base' => array( + 'relative level up is added behind parent path' => array( + 'http://example.com/base/foo/', + '../bar', + 'http://example.com/base/bar', + ), + 'absolute with slash is added behind base without path' => array( 'http://example.com/base', '/test', - 'http://example.com/base/test', + 'http://example.com/test', ), 'query string is added behind base' => array( 'http://example.com/base', @@ -263,10 +258,10 @@ public function provideOtherUris() '?key=value', 'http://example.com/base/?key=value', ), - 'query string with slash is added behind base' => array( + 'query string with slash is added behind base without path' => array( 'http://example.com/base', '/?key=value', - 'http://example.com/base/?key=value', + 'http://example.com/?key=value', ), 'absolute with query string below base is returned as-is' => array( 'http://example.com/base', From 4ce74d248187a9c7026edb43fb3a21e39b01ba1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 Jul 2020 01:33:12 +0200 Subject: [PATCH 034/152] Remove unneeded MessageFactory helper methods --- src/Browser.php | 2 +- src/Io/MessageFactory.php | 24 ------------- src/Io/Transaction.php | 3 +- tests/Io/MessageFactoryTest.php | 62 --------------------------------- 4 files changed, 3 insertions(+), 88 deletions(-) diff --git a/src/Browser.php b/src/Browser.php index 622404bf..48e64fcb 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -585,7 +585,7 @@ public function withBase($baseUrl) return $browser; } - $browser->baseUrl = $this->messageFactory->uri($baseUrl); + $browser->baseUrl = new Uri($baseUrl); if (!\in_array($browser->baseUrl->getScheme(), array('http', 'https')) || $browser->baseUrl->getHost() === '') { throw new \InvalidArgumentException('Base URL must be absolute'); } diff --git a/src/Io/MessageFactory.php b/src/Io/MessageFactory.php index 5b7a2b35..f3d0993d 100644 --- a/src/Io/MessageFactory.php +++ b/src/Io/MessageFactory.php @@ -6,7 +6,6 @@ use Psr\Http\Message\UriInterface; use RingCentral\Psr7\Request; use RingCentral\Psr7\Response; -use RingCentral\Psr7\Uri; use React\Stream\ReadableStreamInterface; /** @@ -76,27 +75,4 @@ public function body($body) return \RingCentral\Psr7\stream_for($body); } - - /** - * Creates a new instance of UriInterface for the given URI string - * - * @param string $uri - * @return UriInterface - */ - public function uri($uri) - { - return new Uri($uri); - } - - /** - * Creates a new instance of UriInterface for the given URI string relative to the given base URI - * - * @param UriInterface $base - * @param string $uri - * @return UriInterface - */ - public function uriRelative(UriInterface $base, $uri) - { - return Uri::resolve($base, $uri); - } } diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index a593e684..93741dcc 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -5,6 +5,7 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; +use RingCentral\Psr7\Uri; use React\EventLoop\LoopInterface; use React\Http\Message\ResponseException; use React\Promise\Deferred; @@ -246,7 +247,7 @@ public function onResponse(ResponseInterface $response, RequestInterface $reques private function onResponseRedirect(ResponseInterface $response, RequestInterface $request, Deferred $deferred) { // resolve location relative to last request URI - $location = $this->messageFactory->uriRelative($request->getUri(), $response->getHeaderLine('Location')); + $location = Uri::resolve($request->getUri(), $response->getHeaderLine('Location')); $request = $this->makeRedirectRequest($request, $location); $this->progress('redirect', array($request)); diff --git a/tests/Io/MessageFactoryTest.php b/tests/Io/MessageFactoryTest.php index b09a4af8..ae2dcdf9 100644 --- a/tests/Io/MessageFactoryTest.php +++ b/tests/Io/MessageFactoryTest.php @@ -17,68 +17,6 @@ public function setUpMessageFactory() $this->messageFactory = new MessageFactory(); } - public function testUriSimple() - { - $uri = $this->messageFactory->uri('http://www.lueck.tv/'); - - $this->assertEquals('http', $uri->getScheme()); - $this->assertEquals('www.lueck.tv', $uri->getHost()); - $this->assertEquals('/', $uri->getPath()); - - $this->assertEquals(null, $uri->getPort()); - $this->assertEquals('', $uri->getQuery()); - } - - public function testUriComplete() - { - $uri = $this->messageFactory->uri('https://example.com:8080/?just=testing'); - - $this->assertEquals('https', $uri->getScheme()); - $this->assertEquals('example.com', $uri->getHost()); - $this->assertEquals(8080, $uri->getPort()); - $this->assertEquals('/', $uri->getPath()); - $this->assertEquals('just=testing', $uri->getQuery()); - } - - public function testPlaceholdersInUriWillBeEscaped() - { - $uri = $this->messageFactory->uri('http://example.com/%7Bversion%7D'); - - $this->assertEquals('/%7Bversion%7D', $uri->getPath()); - } - - public function testEscapedPlaceholdersInUriWillStayEscaped() - { - $uri = $this->messageFactory->uri('http://example.com/%7Bversion%7D'); - - $this->assertEquals('/%7Bversion%7D', $uri->getPath()); - } - - public function testResolveRelative() - { - $base = $this->messageFactory->uri('http://example.com/base/'); - - $this->assertEquals('http://example.com/base/', $this->messageFactory->uriRelative($base, '')); - $this->assertEquals('http://example.com/', $this->messageFactory->uriRelative($base, '/')); - - $this->assertEquals('http://example.com/base/a', $this->messageFactory->uriRelative($base, 'a')); - $this->assertEquals('http://example.com/a', $this->messageFactory->uriRelative($base, '../a')); - } - - public function testResolveAbsolute() - { - $base = $this->messageFactory->uri('http://example.org/'); - - $this->assertEquals('http://www.example.com/', $this->messageFactory->uriRelative($base, 'http://www.example.com/')); - } - - public function testResolveUri() - { - $base = $this->messageFactory->uri('http://example.org/'); - - $this->assertEquals('http://www.example.com/', $this->messageFactory->uriRelative($base, $this->messageFactory->uri('http://www.example.com/'))); - } - public function testBodyString() { $body = $this->messageFactory->body('hi'); From 788526eec3b44b80d830b822e3a5573d85465564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 Jul 2020 13:15:01 +0200 Subject: [PATCH 035/152] Internal refactoring, remove unneeded MessageFactory helper class This is an internal preparation only and does not affect any public APIs. Some internal logic has been refactored and moved to classes with better cohesion. This is done in preparation for upcoming improvements to the `Transfer-Encoding: chunked` response header. --- src/Browser.php | 20 ++-- src/Client/Response.php | 15 ++- src/Io/MessageFactory.php | 78 ------------ src/Io/Sender.php | 29 +++-- src/Io/Transaction.php | 14 +-- tests/FunctionalBrowserTest.php | 62 ++++++++-- tests/Io/MessageFactoryTest.php | 135 --------------------- tests/Io/SenderTest.php | 36 +++--- tests/Io/TransactionTest.php | 204 +++++++++++++------------------- 9 files changed, 199 insertions(+), 394 deletions(-) delete mode 100644 src/Io/MessageFactory.php delete mode 100644 tests/Io/MessageFactoryTest.php diff --git a/src/Browser.php b/src/Browser.php index 48e64fcb..91604994 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -3,9 +3,10 @@ namespace React\Http; use Psr\Http\Message\ResponseInterface; +use RingCentral\Psr7\Request; use RingCentral\Psr7\Uri; use React\EventLoop\LoopInterface; -use React\Http\Io\MessageFactory; +use React\Http\Io\ReadableBodyStream; use React\Http\Io\Sender; use React\Http\Io\Transaction; use React\Promise\PromiseInterface; @@ -19,7 +20,6 @@ class Browser { private $transaction; - private $messageFactory; private $baseUrl; private $protocolVersion = '1.1'; @@ -59,10 +59,8 @@ class Browser */ public function __construct(LoopInterface $loop, ConnectorInterface $connector = null) { - $this->messageFactory = new MessageFactory(); $this->transaction = new Transaction( - Sender::createFromLoop($loop, $connector, $this->messageFactory), - $this->messageFactory, + Sender::createFromLoop($loop, $connector), $loop ); } @@ -721,18 +719,22 @@ private function withOptions(array $options) * @param string $method * @param string $url * @param array $headers - * @param string|ReadableStreamInterface $contents + * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - private function requestMayBeStreaming($method, $url, array $headers = array(), $contents = '') + 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); } - $request = $this->messageFactory->request($method, $url, $headers, $contents, $this->protocolVersion); + if ($body instanceof ReadableStreamInterface) { + $body = new ReadableBodyStream($body); + } - return $this->transaction->send($request); + return $this->transaction->send( + new Request($method, $url, $headers, $body, $this->protocolVersion) + ); } } diff --git a/src/Client/Response.php b/src/Client/Response.php index be19eb4c..2de64bb0 100644 --- a/src/Client/Response.php +++ b/src/Client/Response.php @@ -86,11 +86,24 @@ private function getHeader($name) return isset($normalized[$name]) ? (array)$normalized[$name] : array(); } - private function getHeaderLine($name) + /** + * @param string $name + * @return string + */ + public function getHeaderLine($name) { return implode(', ' , $this->getHeader($name)); } + /** + * @param string $name + * @return bool + */ + public function hasHeader($name) + { + return $this->getHeader($name) !== array(); + } + /** @internal */ public function handleData($data) { diff --git a/src/Io/MessageFactory.php b/src/Io/MessageFactory.php deleted file mode 100644 index f3d0993d..00000000 --- a/src/Io/MessageFactory.php +++ /dev/null @@ -1,78 +0,0 @@ -body($content), $protocolVersion); - } - - /** - * Creates a new instance of ResponseInterface for the given response parameters - * - * @param string $protocolVersion - * @param int $status - * @param string $reason - * @param array $headers - * @param ReadableStreamInterface|string $body - * @param ?string $requestMethod - * @return Response - * @uses self::body() - */ - public function response($protocolVersion, $status, $reason, $headers = array(), $body = '', $requestMethod = null) - { - $response = new Response($status, $headers, $body instanceof ReadableStreamInterface ? null : $body, $protocolVersion, $reason); - - if ($body instanceof ReadableStreamInterface) { - $length = null; - $code = $response->getStatusCode(); - if ($requestMethod === 'HEAD' || ($code >= 100 && $code < 200) || $code == 204 || $code == 304) { - $length = 0; - } elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') { - $length = null; - } elseif ($response->hasHeader('Content-Length')) { - $length = (int)$response->getHeaderLine('Content-Length'); - } - - $response = $response->withBody(new ReadableBodyStream($body, $length)); - } - - return $response; - } - - /** - * Creates a new instance of StreamInterface for the given body contents - * - * @param ReadableStreamInterface|string $body - * @return StreamInterface - */ - public function body($body) - { - if ($body instanceof ReadableStreamInterface) { - return new ReadableBodyStream($body); - } - - return \RingCentral\Psr7\stream_for($body); - } -} diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 6f3367e5..6cba0495 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -6,6 +6,7 @@ use React\EventLoop\LoopInterface; use React\Http\Client\Client as HttpClient; use React\Http\Client\Response as ResponseStream; +use React\Http\Message\Response; use React\Promise\PromiseInterface; use React\Promise\Deferred; use React\Socket\ConnectorInterface; @@ -47,13 +48,12 @@ class Sender * @param ConnectorInterface|null $connector * @return self */ - public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector = null, MessageFactory $messageFactory) + public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector = null) { - return new self(new HttpClient($loop, $connector), $messageFactory); + return new self(new HttpClient($loop, $connector)); } private $http; - private $messageFactory; /** * [internal] Instantiate Sender @@ -61,10 +61,9 @@ public static function createFromLoop(LoopInterface $loop, ConnectorInterface $c * @param HttpClient $http * @internal */ - public function __construct(HttpClient $http, MessageFactory $messageFactory) + public function __construct(HttpClient $http) { $this->http = $http; - $this->messageFactory = $messageFactory; } /** @@ -109,16 +108,22 @@ public function send(RequestInterface $request) $deferred->reject($error); }); - $messageFactory = $this->messageFactory; - $requestStream->on('response', function (ResponseStream $responseStream) use ($deferred, $messageFactory, $request) { + $requestStream->on('response', function (ResponseStream $responseStream) use ($deferred, $request) { + $length = null; + $code = $responseStream->getCode(); + if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code == 204 || $code == 304) { + $length = 0; + } elseif ($responseStream->hasHeader('Content-Length')) { + $length = (int) $responseStream->getHeaderLine('Content-Length'); + } + // apply response header values from response stream - $deferred->resolve($messageFactory->response( - $responseStream->getVersion(), + $deferred->resolve(new Response( $responseStream->getCode(), - $responseStream->getReasonPhrase(), $responseStream->getHeaders(), - $responseStream, - $request->getMethod() + new ReadableBodyStream($responseStream, $length), + $responseStream->getVersion(), + $responseStream->getReasonPhrase() )); }); diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index 93741dcc..9449503f 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -5,6 +5,7 @@ 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\ResponseException; @@ -18,7 +19,6 @@ class Transaction { private $sender; - private $messageFactory; private $loop; // context: http.timeout (ini_get('default_socket_timeout'): 60) @@ -37,10 +37,9 @@ class Transaction private $maximumSize = 16777216; // 16 MiB = 2^24 bytes - public function __construct(Sender $sender, MessageFactory $messageFactory, LoopInterface $loop) + public function __construct(Sender $sender, LoopInterface $loop) { $this->sender = $sender; - $this->messageFactory = $messageFactory; $this->loop = $loop; } @@ -55,7 +54,7 @@ public function withOptions(array $options) if (property_exists($transaction, $name)) { // restore default value if null is given if ($value === null) { - $default = new self($this->sender, $this->messageFactory, $this->loop); + $default = new self($this->sender, $this->loop); $value = $default->$name; } @@ -186,11 +185,10 @@ public function bufferResponse(ResponseInterface $response, $deferred) } // buffer stream and resolve with buffered body - $messageFactory = $this->messageFactory; $maximumSize = $this->maximumSize; $promise = \React\Promise\Stream\buffer($stream, $maximumSize)->then( - function ($body) use ($response, $messageFactory) { - return $response->withBody($messageFactory->body($body)); + function ($body) use ($response) { + return $response->withBody(\RingCentral\Psr7\stream_for($body)); }, function ($e) use ($stream, $maximumSize) { // try to close stream if buffering fails (or is cancelled) @@ -280,7 +278,7 @@ private function makeRedirectRequest(RequestInterface $request, UriInterface $lo // naïve approach.. $method = ($request->getMethod() === 'HEAD') ? 'HEAD' : 'GET'; - return $this->messageFactory->request($method, $location, $request->getHeaders()); + return new Request($method, $location, $request->getHeaders()); } private function progress($name, array $args = array()) diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index f4495565..59cba0b9 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -64,14 +64,22 @@ public function setUpBrowserAndServer() ); } - if ($path === '/status/300') { + if ($path === '/status/204') { return new Response( - 300, + 204, array(), '' ); } + if ($path === '/status/304') { + return new Response( + 304, + array(), + 'Not modified' + ); + } + if ($path === '/status/404') { return new Response( 404, @@ -308,12 +316,24 @@ public function testFollowRedirectsZeroRejectsOnRedirect() Block\await($browser->get($this->base . 'redirect-to?url=get'), $this->loop); } - /** - * @doesNotPerformAssertions - */ - public function testResponseStatus300WithoutLocationShouldResolveWithoutFollowingRedirect() + public function testResponseStatus204ShouldResolveWithEmptyBody() { - Block\await($this->browser->get($this->base . 'status/300'), $this->loop); + $response = Block\await($this->browser->get($this->base . 'status/204'), $this->loop); + $this->assertFalse($response->hasHeader('Content-Length')); + + $body = $response->getBody(); + $this->assertEquals(0, $body->getSize()); + $this->assertEquals('', (string) $body); + } + + public function testResponseStatus304ShouldResolveWithEmptyBodyButContentLengthResponseHeader() + { + $response = Block\await($this->browser->get($this->base . 'status/304'), $this->loop); + $this->assertEquals('12', $response->getHeaderLine('Content-Length')); + + $body = $response->getBody(); + $this->assertEquals(0, $body->getSize()); + $this->assertEquals('', (string) $body); } /** @@ -595,9 +615,33 @@ public function testSendsExplicitHttp10Request() public function testHeadRequestReceivesResponseWithEmptyBodyButWithContentLengthResponseHeader() { $response = Block\await($this->browser->head($this->base . 'get'), $this->loop); - $this->assertEquals('', (string)$response->getBody()); - $this->assertEquals(0, $response->getBody()->getSize()); $this->assertEquals('5', $response->getHeaderLine('Content-Length')); + + $body = $response->getBody(); + $this->assertEquals(0, $body->getSize()); + $this->assertEquals('', (string) $body); + } + + public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndKnownSize() + { + $response = Block\await($this->browser->requestStreaming('GET', $this->base . 'get'), $this->loop); + $this->assertEquals('5', $response->getHeaderLine('Content-Length')); + + $body = $response->getBody(); + $this->assertEquals(5, $body->getSize()); + $this->assertEquals('', (string) $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + } + + public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndUnknownSizeFromStreamingEndpoint() + { + $response = Block\await($this->browser->requestStreaming('GET', $this->base . 'stream/1'), $this->loop); + $this->assertFalse($response->hasHeader('Content-Length')); + + $body = $response->getBody(); + $this->assertNull($body->getSize()); + $this->assertEquals('', (string) $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); } public function testRequestStreamingGetReceivesStreamingResponseBody() diff --git a/tests/Io/MessageFactoryTest.php b/tests/Io/MessageFactoryTest.php deleted file mode 100644 index ae2dcdf9..00000000 --- a/tests/Io/MessageFactoryTest.php +++ /dev/null @@ -1,135 +0,0 @@ -messageFactory = new MessageFactory(); - } - - public function testBodyString() - { - $body = $this->messageFactory->body('hi'); - - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertNotInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertEquals(2, $body->getSize()); - $this->assertEquals('hi', (string)$body); - } - - public function testBodyReadableStream() - { - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - $body = $this->messageFactory->body($stream); - - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertEquals(null, $body->getSize()); - $this->assertEquals('', (string)$body); - } - - public function testResponseWithBodyString() - { - $response = $this->messageFactory->response('1.1', 200, 'OK', array(), 'hi'); - - $body = $response->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertNotInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertEquals(2, $body->getSize()); - $this->assertEquals('hi', (string)$body); - } - - public function testResponseWithStreamingBodyHasUnknownSizeByDefault() - { - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - $response = $this->messageFactory->response('1.1', 200, 'OK', array(), $stream); - - $body = $response->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertNull($body->getSize()); - $this->assertEquals('', (string)$body); - } - - public function testResponseWithStreamingBodyHasSizeFromContentLengthHeader() - { - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - $response = $this->messageFactory->response('1.1', 200, 'OK', array('Content-Length' => '100'), $stream); - - $body = $response->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertEquals(100, $body->getSize()); - $this->assertEquals('', (string)$body); - } - - public function testResponseWithStreamingBodyHasUnknownSizeWithTransferEncodingChunkedHeader() - { - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - $response = $this->messageFactory->response('1.1', 200, 'OK', array('Transfer-Encoding' => 'chunked'), $stream); - - $body = $response->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertNull($body->getSize()); - $this->assertEquals('', (string)$body); - } - - public function testResponseWithStreamingBodyHasZeroSizeForInformationalResponse() - { - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - $response = $this->messageFactory->response('1.1', 101, 'OK', array('Content-Length' => '100'), $stream); - - $body = $response->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertEquals(0, $body->getSize()); - $this->assertEquals('', (string)$body); - } - - public function testResponseWithStreamingBodyHasZeroSizeForNoContentResponse() - { - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - $response = $this->messageFactory->response('1.1', 204, 'OK', array('Content-Length' => '100'), $stream); - - $body = $response->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertEquals(0, $body->getSize()); - $this->assertEquals('', (string)$body); - } - - public function testResponseWithStreamingBodyHasZeroSizeForNotModifiedResponse() - { - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - $response = $this->messageFactory->response('1.1', 304, 'OK', array('Content-Length' => '100'), $stream); - - $body = $response->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertEquals(0, $body->getSize()); - $this->assertEquals('', (string)$body); - } - - public function testResponseWithStreamingBodyHasZeroSizeForHeadRequestMethod() - { - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); - $response = $this->messageFactory->response('1.1', 200, 'OK', array('Content-Length' => '100'), $stream, 'HEAD'); - - $body = $response->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); - $this->assertEquals(0, $body->getSize()); - $this->assertEquals('', (string)$body); - } -} diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 35eb22e7..1c6d1d6b 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -26,7 +26,7 @@ public function setUpLoop() public function testCreateFromLoop() { - $sender = Sender::createFromLoop($this->loop, null, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = Sender::createFromLoop($this->loop, null); $this->assertInstanceOf('React\Http\Io\Sender', $sender); } @@ -36,7 +36,7 @@ public function testSenderRejectsInvalidUri() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->never())->method('connect'); - $sender = new Sender(new HttpClient($this->loop, $connector), $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender(new HttpClient($this->loop, $connector)); $request = new Request('GET', 'www.google.com'); @@ -51,7 +51,7 @@ 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), $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender(new HttpClient($this->loop, $connector)); $request = new Request('GET', 'http://www.google.com/'); @@ -71,7 +71,7 @@ public function testSendPostWillAutomaticallySendContentLengthHeader() '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $request = new Request('POST', 'http://www.google.com/', array(), 'hello'); $sender->send($request); @@ -87,7 +87,7 @@ public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmpty '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $request = new Request('POST', 'http://www.google.com/', array(), ''); $sender->send($request); @@ -106,7 +106,7 @@ public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() '1.1' )->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -122,7 +122,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAn $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -142,7 +142,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -162,7 +162,7 @@ public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $expected = new \RuntimeException(); $stream = new ThroughStream(); @@ -192,7 +192,7 @@ public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -220,7 +220,7 @@ public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); @@ -247,7 +247,7 @@ public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $stream = new ThroughStream(); $request = new Request('POST', 'http://www.google.com/', array('Content-Length' => '100'), new ReadableBodyStream($stream)); @@ -264,7 +264,7 @@ public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $request = new Request('GET', 'http://www.google.com/'); $sender->send($request); @@ -280,7 +280,7 @@ public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyReques '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $request = new Request('CUSTOM', 'http://www.google.com/'); $sender->send($request); @@ -296,7 +296,7 @@ public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsI '1.1' )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); - $sender = new Sender($client, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($client); $request = new Request('CUSTOM', 'http://www.google.com/', array('Content-Length' => '0')); $sender->send($request); @@ -311,7 +311,7 @@ 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), $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender(new HttpClient($this->loop, $connector)); $request = new Request('GET', 'http://www.google.com/'); @@ -330,7 +330,7 @@ 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), $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender(new HttpClient($this->loop, $connector)); $request = new Request('GET', 'http://www.google.com/'); @@ -387,7 +387,7 @@ public function testRequestProtocolVersion(Request $Request, $method, $uri, $hea $http->expects($this->once())->method('request')->with($method, $uri, $headers, $protocolVersion)->willReturn($request); - $sender = new Sender($http, $this->getMockBuilder('React\Http\Io\MessageFactory')->getMock()); + $sender = new Sender($http); $sender->send($Request); } } diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 88320bf0..384a74a6 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -5,7 +5,7 @@ use Clue\React\Block; use PHPUnit\Framework\MockObject\MockObject; use Psr\Http\Message\RequestInterface; -use React\Http\Io\MessageFactory; +use RingCentral\Psr7\Response; use React\Http\Io\Transaction; use React\Http\Message\ResponseException; use React\EventLoop\Factory; @@ -13,7 +13,8 @@ use React\Promise\Deferred; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; -use RingCentral\Psr7\Response; +use RingCentral\Psr7\Request; +use React\Http\Io\ReadableBodyStream; class TransactionTest extends TestCase { @@ -21,7 +22,7 @@ public function testWithOptionsReturnsNewInstanceWithChangedOption() { $sender = $this->makeSenderMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $transaction = new Transaction($sender, new MessageFactory(), $loop); + $transaction = new Transaction($sender, $loop); $new = $transaction->withOptions(array('followRedirects' => false)); @@ -38,7 +39,7 @@ public function testWithOptionsDoesNotChangeOriginalInstance() { $sender = $this->makeSenderMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $transaction = new Transaction($sender, new MessageFactory(), $loop); + $transaction = new Transaction($sender, $loop); $transaction->withOptions(array('followRedirects' => false)); @@ -52,7 +53,7 @@ public function testWithOptionsNullValueReturnsNewInstanceWithDefaultOption() { $sender = $this->makeSenderMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $transaction = new Transaction($sender, new MessageFactory(), $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('followRedirects' => false)); $transaction = $transaction->withOptions(array('followRedirects' => null)); @@ -65,8 +66,6 @@ public function testWithOptionsNullValueReturnsNewInstanceWithDefaultOption() public function testTimeoutExplicitOptionWillStartTimeoutTimer() { - $messageFactory = new MessageFactory(); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); @@ -77,7 +76,7 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimer() $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 2)); $promise = $transaction->send($request); @@ -86,8 +85,6 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimer() public function testTimeoutImplicitFromIniWillStartTimeoutTimer() { - $messageFactory = new MessageFactory(); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); @@ -98,7 +95,7 @@ public function testTimeoutImplicitFromIniWillStartTimeoutTimer() $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $old = ini_get('default_socket_timeout'); ini_set('default_socket_timeout', '2'); @@ -110,8 +107,6 @@ public function testTimeoutImplicitFromIniWillStartTimeoutTimer() public function testTimeoutExplicitOptionWillRejectWhenTimerFires() { - $messageFactory = new MessageFactory(); - $timeout = null; $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -126,7 +121,7 @@ public function testTimeoutExplicitOptionWillRejectWhenTimerFires() $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 2)); $promise = $transaction->send($request); @@ -144,18 +139,16 @@ public function testTimeoutExplicitOptionWillRejectWhenTimerFires() public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderResolvesImmediately() { - $messageFactory = new MessageFactory(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addTimer'); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = $messageFactory->response(1.0, 200, 'OK', array(), ''); + $response = new Response(200, array(), ''); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 0.001)); $promise = $transaction->send($request); @@ -165,21 +158,19 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderResolvesIm public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderResolvesLaterOn() { - $messageFactory = new MessageFactory(); - $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); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = $messageFactory->response(1.0, 200, 'OK', array(), ''); + $response = new Response(200, array(), ''); $deferred = new Deferred(); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 0.001)); $promise = $transaction->send($request); @@ -191,8 +182,6 @@ public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderResolve public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderRejectsImmediately() { - $messageFactory = new MessageFactory(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addTimer'); @@ -202,7 +191,7 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderRejectsImm $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\reject($exception)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 0.001)); $promise = $transaction->send($request); @@ -212,8 +201,6 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderRejectsImm public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderRejectsLaterOn() { - $messageFactory = new MessageFactory(); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->willReturn($timer); @@ -225,7 +212,7 @@ public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderRejects $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 0.001)); $promise = $transaction->send($request); @@ -238,8 +225,6 @@ public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderRejects public function testTimeoutExplicitNegativeWillNotStartTimeoutTimer() { - $messageFactory = new MessageFactory(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addTimer'); @@ -248,7 +233,7 @@ public function testTimeoutExplicitNegativeWillNotStartTimeoutTimer() $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => -1)); $promise = $transaction->send($request); @@ -257,18 +242,16 @@ public function testTimeoutExplicitNegativeWillNotStartTimeoutTimer() public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenRequestBodyIsStreaming() { - $messageFactory = new MessageFactory(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addTimer'); $stream = new ThroughStream(); - $request = $messageFactory->request('POST', 'http://example.com/', array(), $stream); + $request = new Request('POST', 'http://example.com/', array(), new ReadableBodyStream($stream)); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 2)); $promise = $transaction->send($request); @@ -277,8 +260,6 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenRequestBody public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingRequestBodyIsAlreadyClosed() { - $messageFactory = new MessageFactory(); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); @@ -286,12 +267,12 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingReque $stream = new ThroughStream(); $stream->close(); - $request = $messageFactory->request('POST', 'http://example.com/', array(), $stream); + $request = new Request('POST', 'http://example.com/', array(), new ReadableBodyStream($stream)); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 2)); $promise = $transaction->send($request); @@ -300,20 +281,18 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingReque public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingRequestBodyClosesWhileSenderIsStillPending() { - $messageFactory = new MessageFactory(); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); $stream = new ThroughStream(); - $request = $messageFactory->request('POST', 'http://example.com/', array(), $stream); + $request = new Request('POST', 'http://example.com/', array(), new ReadableBodyStream($stream)); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 2)); $promise = $transaction->send($request); @@ -324,19 +303,17 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingReque public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenStreamingRequestBodyClosesAfterSenderRejects() { - $messageFactory = new MessageFactory(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addTimer'); $stream = new ThroughStream(); - $request = $messageFactory->request('POST', 'http://example.com/', array(), $stream); + $request = new Request('POST', 'http://example.com/', array(), new ReadableBodyStream($stream)); $deferred = new Deferred(); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 2)); $promise = $transaction->send($request); @@ -348,8 +325,6 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenStreamingRe public function testTimeoutExplicitOptionWillRejectWhenTimerFiresAfterStreamingRequestBodyCloses() { - $messageFactory = new MessageFactory(); - $timeout = null; $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -360,12 +335,12 @@ public function testTimeoutExplicitOptionWillRejectWhenTimerFiresAfterStreamingR $loop->expects($this->never())->method('cancelTimer'); $stream = new ThroughStream(); - $request = $messageFactory->request('POST', 'http://example.com/', array(), $stream); + $request = new Request('POST', 'http://example.com/', array(), new ReadableBodyStream($stream)); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 2)); $promise = $transaction->send($request); @@ -393,7 +368,7 @@ public function testReceivingErrorResponseWillRejectWithResponseException() $sender = $this->makeSenderMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); - $transaction = new Transaction($sender, new MessageFactory(), $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => -1)); $promise = $transaction->send($request); @@ -408,7 +383,6 @@ public function testReceivingErrorResponseWillRejectWithResponseException() public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefault() { - $messageFactory = new MessageFactory(); $loop = Factory::create(); $stream = new ThroughStream(); @@ -418,13 +392,13 @@ public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefau }); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = $messageFactory->response(1.0, 200, 'OK', array(), $stream); + $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, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); $response = Block\await($promise, $loop); @@ -435,7 +409,6 @@ public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefau public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBufferWillRejectAndCloseResponseStream() { - $messageFactory = new MessageFactory(); $loop = Factory::create(); $stream = new ThroughStream(); @@ -443,13 +416,13 @@ public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBuffer $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = $messageFactory->response(1.0, 200, 'OK', array('Content-Length' => '100000000'), $stream); + $response = new Response(200, array('Content-Length' => '100000000'), new ReadableBodyStream($stream, 100000000)); // 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, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); $this->setExpectedException('OverflowException'); @@ -458,7 +431,6 @@ public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBuffer public function testCancelBufferingResponseWillCloseStreamAndReject() { - $messageFactory = new MessageFactory(); $loop = Factory::create(); $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); @@ -466,13 +438,13 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() $stream->expects($this->once())->method('close'); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = $messageFactory->response(1.0, 200, 'OK', array(), $stream); + $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, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); $promise->cancel(); @@ -482,17 +454,16 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStreamingIsEnabled() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = $messageFactory->response(1.0, 200, 'OK', array(), $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock()); + $response = new Response(200, array(), new ReadableBodyStream($this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock())); // 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, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('streaming' => true, 'timeout' => -1)); $promise = $transaction->send($request); @@ -504,16 +475,15 @@ public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStre public function testResponseCode304WithoutLocationWillResolveWithResponseAsIs() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); // conditional GET request will respond with 304 (Not Modified - $request = $messageFactory->request('GET', 'http://example.com/', array('If-None-Match' => '"abc"')); - $response = $messageFactory->response(1.0, 304, null, array('ETag' => '"abc"')); + $request = new Request('GET', 'http://example.com/', array('If-None-Match' => '"abc"')); + $response = new Response(304, array('ETag' => '"abc"')); $sender = $this->makeSenderMock(); $sender->expects($this->once())->method('send')->with($request)->willReturn(Promise\resolve($response)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => -1)); $promise = $transaction->send($request); @@ -522,12 +492,11 @@ public function testResponseCode304WithoutLocationWillResolveWithResponseAsIs() public function testCustomRedirectResponseCode333WillFollowLocationHeaderAndSendRedirectedRequest() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); // original GET request will respond with custom 333 redirect status code and follow location header - $requestOriginal = $messageFactory->request('GET', 'http://example.com/'); - $response = $messageFactory->response(1.0, 333, null, array('Location' => 'foo')); + $requestOriginal = new Request('GET', 'http://example.com/'); + $response = new Response(333, array('Location' => 'foo')); $sender = $this->makeSenderMock(); $sender->expects($this->exactly(2))->method('send')->withConsecutive( array($requestOriginal), @@ -539,27 +508,26 @@ public function testCustomRedirectResponseCode333WillFollowLocationHeaderAndSend new \React\Promise\Promise(function () { }) ); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction->send($requestOriginal); } public function testFollowingRedirectWithSpecifiedHeaders() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $customHeaders = array('User-Agent' => 'Chrome'); - $requestWithUserAgent = $messageFactory->request('GET', 'http://example.com/', $customHeaders); + $requestWithUserAgent = new Request('GET', 'http://example.com/', $customHeaders); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithUserAgent - $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://redirect.com/')); + $redirectResponse = new Response(301, array('Location' => 'http://redirect.com/')); $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithUserAgent - $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $okResponse = new Response(200); $that = $this; $sender->expects($this->at(1)) ->method('send') @@ -568,27 +536,26 @@ public function testFollowingRedirectWithSpecifiedHeaders() return true; }))->willReturn(Promise\resolve($okResponse)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction->send($requestWithUserAgent); } public function testRemovingAuthorizationHeaderWhenChangingHostnamesDuringRedirect() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $customHeaders = array('Authorization' => 'secret'); - $requestWithAuthorization = $messageFactory->request('GET', 'http://example.com/', $customHeaders); + $requestWithAuthorization = new Request('GET', 'http://example.com/', $customHeaders); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithAuthorization - $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://redirect.com/')); + $redirectResponse = new Response(301, array('Location' => 'http://redirect.com/')); $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithAuthorization - $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $okResponse = new Response(200); $that = $this; $sender->expects($this->at(1)) ->method('send') @@ -597,27 +564,26 @@ public function testRemovingAuthorizationHeaderWhenChangingHostnamesDuringRedire return true; }))->willReturn(Promise\resolve($okResponse)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction->send($requestWithAuthorization); } public function testAuthorizationHeaderIsForwardedWhenRedirectingToSameDomain() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $customHeaders = array('Authorization' => 'secret'); - $requestWithAuthorization = $messageFactory->request('GET', 'http://example.com/', $customHeaders); + $requestWithAuthorization = new Request('GET', 'http://example.com/', $customHeaders); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithAuthorization - $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new')); + $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithAuthorization - $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $okResponse = new Response(200); $that = $this; $sender->expects($this->at(1)) ->method('send') @@ -626,26 +592,25 @@ public function testAuthorizationHeaderIsForwardedWhenRedirectingToSameDomain() return true; }))->willReturn(Promise\resolve($okResponse)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction->send($requestWithAuthorization); } public function testAuthorizationHeaderIsForwardedWhenLocationContainsAuthentication() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $request = $messageFactory->request('GET', 'http://example.com/'); + $request = new Request('GET', 'http://example.com/'); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithAuthorization - $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://user:pass@example.com/new')); + $redirectResponse = new Response(301, array('Location' => 'http://user:pass@example.com/new')); $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithAuthorization - $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $okResponse = new Response(200); $that = $this; $sender->expects($this->at(1)) ->method('send') @@ -655,13 +620,12 @@ public function testAuthorizationHeaderIsForwardedWhenLocationContainsAuthentica return true; }))->willReturn(Promise\resolve($okResponse)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction->send($request); } public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $customHeaders = array( @@ -669,17 +633,17 @@ public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting() 'Content-Length' => '111', ); - $requestWithCustomHeaders = $messageFactory->request('GET', 'http://example.com/', $customHeaders); + $requestWithCustomHeaders = new Request('GET', 'http://example.com/', $customHeaders); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithCustomHeaders - $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new')); + $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithCustomHeaders - $okResponse = $messageFactory->response(1.0, 200, 'OK'); + $okResponse = new Response(200); $that = $this; $sender->expects($this->at(1)) ->method('send') @@ -689,16 +653,15 @@ public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting() return true; }))->willReturn(Promise\resolve($okResponse)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction->send($requestWithCustomHeaders); } public function testCancelTransactionWillCancelRequest() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $request = $messageFactory->request('GET', 'http://example.com/'); + $request = new Request('GET', 'http://example.com/'); $sender = $this->makeSenderMock(); $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); @@ -706,7 +669,7 @@ public function testCancelTransactionWillCancelRequest() // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->once())->method('send')->willReturn($pending); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); $promise->cancel(); @@ -714,14 +677,12 @@ public function testCancelTransactionWillCancelRequest() public function testCancelTransactionWillCancelTimeoutTimer() { - $messageFactory = new MessageFactory(); - $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); - $request = $messageFactory->request('GET', 'http://example.com/'); + $request = new Request('GET', 'http://example.com/'); $sender = $this->makeSenderMock(); $pending = new \React\Promise\Promise(function () { }, function () { throw new \RuntimeException(); }); @@ -729,7 +690,7 @@ public function testCancelTransactionWillCancelTimeoutTimer() // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->once())->method('send')->willReturn($pending); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(array('timeout' => 2)); $promise = $transaction->send($request); @@ -738,14 +699,13 @@ public function testCancelTransactionWillCancelTimeoutTimer() public function testCancelTransactionWillCancelRedirectedRequest() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $request = $messageFactory->request('GET', 'http://example.com/'); + $request = new Request('GET', 'http://example.com/'); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in - $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new')); + $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); @@ -753,7 +713,7 @@ public function testCancelTransactionWillCancelRedirectedRequest() // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->at(1))->method('send')->willReturn($pending); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); $promise->cancel(); @@ -761,10 +721,9 @@ public function testCancelTransactionWillCancelRedirectedRequest() public function testCancelTransactionWillCancelRedirectedRequestAgain() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $request = $messageFactory->request('GET', 'http://example.com/'); + $request = new Request('GET', 'http://example.com/'); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in @@ -776,31 +735,30 @@ public function testCancelTransactionWillCancelRedirectedRequestAgain() // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->at(1))->method('send')->willReturn($second); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); // mock sender to resolve promise with the given $redirectResponse in - $first->resolve($messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new'))); + $first->resolve(new Response(301, array('Location' => 'http://example.com/new'))); $promise->cancel(); } public function testCancelTransactionWillCloseBufferingStream() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $request = $messageFactory->request('GET', 'http://example.com/'); + $request = new Request('GET', 'http://example.com/'); $sender = $this->makeSenderMock(); $body = new ThroughStream(); $body->on('close', $this->expectCallableOnce()); // mock sender to resolve promise with the given $redirectResponse in - $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new'), $body); + $redirectResponse = new Response(301, array('Location' => 'http://example.com/new'), new ReadableBodyStream($body)); $sender->expects($this->once())->method('send')->willReturn(Promise\resolve($redirectResponse)); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); $promise->cancel(); @@ -808,36 +766,34 @@ public function testCancelTransactionWillCloseBufferingStream() public function testCancelTransactionWillCloseBufferingStreamAgain() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $request = $messageFactory->request('GET', 'http://example.com/'); + $request = new Request('GET', 'http://example.com/'); $sender = $this->makeSenderMock(); $first = new Deferred(); $sender->expects($this->once())->method('send')->willReturn($first->promise()); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); $body = new ThroughStream(); $body->on('close', $this->expectCallableOnce()); // mock sender to resolve promise with the given $redirectResponse in - $first->resolve($messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new'), $body)); + $first->resolve(new Response(301, array('Location' => 'http://example.com/new'), new ReadableBodyStream($body))); $promise->cancel(); } public function testCancelTransactionShouldCancelSendingPromise() { - $messageFactory = new MessageFactory(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $request = $messageFactory->request('GET', 'http://example.com/'); + $request = new Request('GET', 'http://example.com/'); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in - $redirectResponse = $messageFactory->response(1.0, 301, null, array('Location' => 'http://example.com/new')); + $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); @@ -845,7 +801,7 @@ public function testCancelTransactionShouldCancelSendingPromise() // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->at(1))->method('send')->willReturn($pending); - $transaction = new Transaction($sender, $messageFactory, $loop); + $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); $promise->cancel(); From a9505c6e0f8ea66db6498a606d4b8f7a65400224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 14 Jul 2020 16:05:54 +0200 Subject: [PATCH 036/152] Unify chunked decoder and always skip trailers also for HTTP server Remove dedicated client-side chunked decoder in favor of existing server-side chunked decoder. Unify handling to always skip (unused) trailers for both sides. --- src/Client/ChunkedStreamDecoder.php | 207 --------------------- src/Client/Response.php | 3 +- src/Io/ChunkedDecoder.php | 13 +- tests/Client/DecodeChunkedStreamTest.php | 227 ----------------------- tests/Io/ChunkedDecoderTest.php | 32 +++- 5 files changed, 40 insertions(+), 442 deletions(-) delete mode 100644 src/Client/ChunkedStreamDecoder.php delete mode 100644 tests/Client/DecodeChunkedStreamTest.php diff --git a/src/Client/ChunkedStreamDecoder.php b/src/Client/ChunkedStreamDecoder.php deleted file mode 100644 index 02cab52a..00000000 --- a/src/Client/ChunkedStreamDecoder.php +++ /dev/null @@ -1,207 +0,0 @@ -stream = $stream; - $this->stream->on('data', array($this, 'handleData')); - $this->stream->on('end', array($this, 'handleEnd')); - Util::forwardEvents($this->stream, $this, array( - 'error', - )); - } - - /** @internal */ - public function handleData($data) - { - $this->buffer .= $data; - - do { - $bufferLength = strlen($this->buffer); - $continue = $this->iterateBuffer(); - $iteratedBufferLength = strlen($this->buffer); - } while ( - $continue && - $bufferLength !== $iteratedBufferLength && - $iteratedBufferLength > 0 - ); - - if ($this->buffer === false) { - $this->buffer = ''; - } - } - - protected function iterateBuffer() - { - if (strlen($this->buffer) <= 1) { - return false; - } - - if ($this->nextChunkIsLength) { - $crlfPosition = strpos($this->buffer, static::CRLF); - if ($crlfPosition === false && strlen($this->buffer) > 1024) { - $this->emit('error', array( - new Exception('Chunk length header longer then 1024 bytes'), - )); - $this->close(); - return false; - } - if ($crlfPosition === false) { - return false; // Chunk header hasn't completely come in yet - } - $lengthChunk = substr($this->buffer, 0, $crlfPosition); - if (strpos($lengthChunk, ';') !== false) { - list($lengthChunk) = explode(';', $lengthChunk, 2); - } - if ($lengthChunk !== '') { - $lengthChunk = ltrim(trim($lengthChunk), "0"); - if ($lengthChunk === '') { - // We've reached the end of the stream - $this->reachedEnd = true; - $this->emit('end'); - $this->close(); - return false; - } - } - $this->nextChunkIsLength = false; - if (dechex(@hexdec($lengthChunk)) !== strtolower($lengthChunk)) { - $this->emit('error', array( - new Exception('Unable to validate "' . $lengthChunk . '" as chunk length header'), - )); - $this->close(); - return false; - } - $this->remainingLength = hexdec($lengthChunk); - $this->buffer = substr($this->buffer, $crlfPosition + 2); - return true; - } - - if ($this->remainingLength > 0) { - $chunkLength = $this->getChunkLength(); - if ($chunkLength === 0) { - return true; - } - $this->emit('data', array( - substr($this->buffer, 0, $chunkLength), - $this - )); - $this->remainingLength -= $chunkLength; - $this->buffer = substr($this->buffer, $chunkLength); - return true; - } - - $this->nextChunkIsLength = true; - $this->buffer = substr($this->buffer, 2); - return true; - } - - protected function getChunkLength() - { - $bufferLength = strlen($this->buffer); - - if ($bufferLength >= $this->remainingLength) { - return $this->remainingLength; - } - - return $bufferLength; - } - - public function pause() - { - $this->stream->pause(); - } - - public function resume() - { - $this->stream->resume(); - } - - public function isReadable() - { - return $this->stream->isReadable(); - } - - public function pipe(WritableStreamInterface $dest, array $options = array()) - { - Util::pipe($this, $dest, $options); - - return $dest; - } - - public function close() - { - $this->closed = true; - return $this->stream->close(); - } - - /** @internal */ - public function handleEnd() - { - $this->handleData(''); - - if ($this->closed) { - return; - } - - if ($this->buffer === '' && $this->reachedEnd) { - $this->emit('end'); - $this->close(); - return; - } - - $this->emit( - 'error', - array( - new Exception('Stream ended with incomplete control code') - ) - ); - $this->close(); - } -} diff --git a/src/Client/Response.php b/src/Client/Response.php index 2de64bb0..0510d60b 100644 --- a/src/Client/Response.php +++ b/src/Client/Response.php @@ -3,6 +3,7 @@ namespace React\Http\Client; use Evenement\EventEmitter; +use React\Http\Io\ChunkedDecoder; use React\Stream\ReadableStreamInterface; use React\Stream\Util; use React\Stream\WritableStreamInterface; @@ -33,7 +34,7 @@ public function __construct(ReadableStreamInterface $stream, $protocol, $version $this->headers = $headers; if (strtolower($this->getHeaderLine('Transfer-Encoding')) === 'chunked') { - $this->stream = new ChunkedStreamDecoder($stream); + $this->stream = new ChunkedDecoder($stream); $this->removeHeader('Transfer-Encoding'); } diff --git a/src/Io/ChunkedDecoder.php b/src/Io/ChunkedDecoder.php index f7bbe603..45642a63 100644 --- a/src/Io/ChunkedDecoder.php +++ b/src/Io/ChunkedDecoder.php @@ -116,7 +116,7 @@ public function handleData($data) } if ($hexValue !== '') { - $hexValue = \ltrim($hexValue, "0"); + $hexValue = \ltrim(\trim($hexValue), "0"); if ($hexValue === '') { $hexValue = "0"; } @@ -155,16 +155,19 @@ public function handleData($data) $this->headerCompleted = false; $this->transferredSize = 0; $this->buffer = (string)\substr($this->buffer, 2); + } elseif ($this->chunkSize === 0) { + // end chunk received, skip all trailer data + $this->buffer = (string)\substr($this->buffer, $positionCrlf); } - if ($positionCrlf !== 0 && $this->chunkSize === $this->transferredSize && \strlen($this->buffer) > 2) { - // the first 2 characters are not CLRF, send error event - $this->handleError(new Exception('Chunk does not end with a CLRF')); + if ($positionCrlf !== 0 && $this->chunkSize !== 0 && $this->chunkSize === $this->transferredSize && \strlen($this->buffer) > 2) { + // the first 2 characters are not CRLF, send error event + $this->handleError(new Exception('Chunk does not end with a CRLF')); return; } if ($positionCrlf !== 0 && \strlen($this->buffer) < 2) { - // No CLRF found, wait for additional data which could be a CLRF + // No CRLF found, wait for additional data which could be a CRLF return; } } diff --git a/tests/Client/DecodeChunkedStreamTest.php b/tests/Client/DecodeChunkedStreamTest.php deleted file mode 100644 index f238fb6b..00000000 --- a/tests/Client/DecodeChunkedStreamTest.php +++ /dev/null @@ -1,227 +0,0 @@ - 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' => 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' => 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' => 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' => 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"), - "" - ) - ); - } - - /** - * @test - * @dataProvider provideChunkedEncoding - */ - public function testChunkedEncoding(array $strings, $expected = "Wikipedia in\r\n\r\nchunks.") - { - $stream = new ThroughStream(); - $response = new ChunkedStreamDecoder($stream); - $buffer = ''; - $response->on('data', function ($data) use (&$buffer) { - $buffer .= $data; - }); - $response->on('error', function ($error) { - $this->fail((string)$error); - }); - foreach ($strings as $string) { - $stream->write($string); - } - $this->assertSame($expected, $buffer); - } - - public function provideInvalidChunkedEncoding() - { - 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' => 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") - ) - ); - } - - /** - * @test - * @dataProvider provideInvalidChunkedEncoding - */ - public function testInvalidChunkedEncoding(array $strings) - { - $stream = new ThroughStream(); - $response = new ChunkedStreamDecoder($stream); - $response->on('error', function (Exception $exception) { - throw $exception; - }); - - $this->setExpectedException('Exception'); - foreach ($strings as $string) { - $stream->write($string); - } - } - - public function provideZeroChunk() - { - return array( - array('1-zero' => "0\r\n\r\n"), - array('random-zero' => str_repeat("0", rand(2, 10))."\r\n\r\n") - ); - } - - /** - * @test - * @dataProvider provideZeroChunk - */ - public function testHandleEnd($zeroChunk) - { - $ended = false; - $stream = new ThroughStream(); - $response = new ChunkedStreamDecoder($stream); - $response->on('error', function ($error) { - $this->fail((string)$error); - }); - $response->on('end', function () use (&$ended) { - $ended = true; - }); - - $stream->write("4\r\nWiki\r\n".$zeroChunk); - - $this->assertTrue($ended); - } - - public function testHandleEndIncomplete() - { - $exception = null; - $stream = new ThroughStream(); - $response = new ChunkedStreamDecoder($stream); - $response->on('error', function ($e) use (&$exception) { - $exception = $e; - }); - - $stream->end("4\r\nWiki"); - - $this->assertInstanceOf('Exception', $exception); - } - - public function testHandleEndTrailers() - { - $ended = false; - $stream = new ThroughStream(); - $response = new ChunkedStreamDecoder($stream); - $response->on('error', function ($error) { - $this->fail((string)$error); - }); - $response->on('end', function () use (&$ended) { - $ended = true; - }); - - $stream->write("4\r\nWiki\r\n0\r\nabc: def\r\nghi: klm\r\n\r\n"); - - $this->assertTrue($ended); - } - - /** - * @test - * @dataProvider provideZeroChunk - */ - public function testHandleEndEnsureNoError($zeroChunk) - { - $ended = false; - $stream = new ThroughStream(); - $response = new ChunkedStreamDecoder($stream); - $response->on('error', function ($error) { - $this->fail((string)$error); - }); - $response->on('end', function () use (&$ended) { - $ended = true; - }); - - $stream->write("4\r\nWiki\r\n"); - $stream->write($zeroChunk); - $stream->end(); - - $this->assertTrue($ended); - } -} diff --git a/tests/Io/ChunkedDecoderTest.php b/tests/Io/ChunkedDecoderTest.php index c7889b3f..b78a4cdd 100644 --- a/tests/Io/ChunkedDecoderTest.php +++ b/tests/Io/ChunkedDecoderTest.php @@ -8,7 +8,6 @@ class ChunkedDecoderTest extends TestCase { - /** * @before */ @@ -101,7 +100,6 @@ public function testSplittedHeader() $this->parser->on('end', $this->expectCallableNever());# $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("4")); $this->input->emit('data', array("\r\nwelt\r\n")); } @@ -423,6 +421,36 @@ public function testLeadingZerosInEndChunkWillBeIgnored() $this->input->emit('data', array("0000\r\n\r\n")); } + public function testAdditionalWhitespaceInEndChunkWillBeIgnored() + { + $this->parser->on('data', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableOnce()); + $this->parser->on('close', $this->expectCallableOnce()); + + $this->input->emit('data', array(" 0 \r\n\r\n")); + } + + public function testEndChunkWithTrailersWillBeIgnored() + { + $this->parser->on('data', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableOnce()); + $this->parser->on('close', $this->expectCallableOnce()); + + $this->input->emit('data', array("0\r\nFoo: bar\r\n\r\n")); + } + + public function testEndChunkWithMultipleTrailersWillBeIgnored() + { + $this->parser->on('data', $this->expectCallableNever()); + $this->parser->on('error', $this->expectCallableNever()); + $this->parser->on('end', $this->expectCallableOnce()); + $this->parser->on('close', $this->expectCallableOnce()); + + $this->input->emit('data', array("0\r\nFoo: a\r\nBar: b\r\nBaz: c\r\n\r\n")); + } + public function testLeadingZerosInInvalidChunk() { $this->parser->on('data', $this->expectCallableNever()); From bbe6cd5cc3beb56449a491b23df6c6a07e1e922c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 24 Jul 2020 18:56:26 +0200 Subject: [PATCH 037/152] Expose `Transfer-Encoding: chunked` header and fix chunked HEAD response --- src/Client/Response.php | 16 -------- src/Io/Sender.php | 5 ++- tests/Client/ResponseTest.php | 65 --------------------------------- tests/FunctionalBrowserTest.php | 40 ++++++++++++++++---- 4 files changed, 37 insertions(+), 89 deletions(-) diff --git a/src/Client/Response.php b/src/Client/Response.php index 0510d60b..132e0a54 100644 --- a/src/Client/Response.php +++ b/src/Client/Response.php @@ -3,7 +3,6 @@ namespace React\Http\Client; use Evenement\EventEmitter; -use React\Http\Io\ChunkedDecoder; use React\Stream\ReadableStreamInterface; use React\Stream\Util; use React\Stream\WritableStreamInterface; @@ -33,11 +32,6 @@ public function __construct(ReadableStreamInterface $stream, $protocol, $version $this->reasonPhrase = $reasonPhrase; $this->headers = $headers; - if (strtolower($this->getHeaderLine('Transfer-Encoding')) === 'chunked') { - $this->stream = new ChunkedDecoder($stream); - $this->removeHeader('Transfer-Encoding'); - } - $this->stream->on('data', array($this, 'handleData')); $this->stream->on('error', array($this, 'handleError')); $this->stream->on('end', array($this, 'handleEnd')); @@ -69,16 +63,6 @@ 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); diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 6cba0495..353fa536 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -110,9 +110,12 @@ public function send(RequestInterface $request) $requestStream->on('response', function (ResponseStream $responseStream) use ($deferred, $request) { $length = null; + $body = $responseStream; $code = $responseStream->getCode(); if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code == 204 || $code == 304) { $length = 0; + } elseif (\strtolower($responseStream->getHeaderLine('Transfer-Encoding')) === 'chunked') { + $body = new ChunkedDecoder($body); } elseif ($responseStream->hasHeader('Content-Length')) { $length = (int) $responseStream->getHeaderLine('Content-Length'); } @@ -121,7 +124,7 @@ public function send(RequestInterface $request) $deferred->resolve(new Response( $responseStream->getCode(), $responseStream->getHeaders(), - new ReadableBodyStream($responseStream, $length), + new ReadableBodyStream($body, $length), $responseStream->getVersion(), $responseStream->getReasonPhrase() )); diff --git a/tests/Client/ResponseTest.php b/tests/Client/ResponseTest.php index 14467239..0e757619 100644 --- a/tests/Client/ResponseTest.php +++ b/tests/Client/ResponseTest.php @@ -99,70 +99,5 @@ public function closedResponseShouldNotBeResumedOrPaused() $response->getHeaders() ); } - - /** @test */ - public function chunkedEncodingResponse() - { - $stream = new ThroughStream(); - $response = new Response( - $stream, - 'http', - '1.0', - '200', - 'ok', - array( - 'content-type' => 'text/plain', - 'transfer-encoding' => 'chunked', - ) - ); - - $buffer = ''; - $response->on('data', function ($data) use (&$buffer) { - $buffer.= $data; - }); - $this->assertSame('', $buffer); - $stream->write("4; abc=def\r\n"); - $this->assertSame('', $buffer); - $stream->write("Wiki\r\n"); - $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/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 59cba0b9..f5b7f324 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -462,7 +462,7 @@ public function testPostString() $this->assertEquals('hello world', $data['data']); } - public function testReceiveStreamUntilConnectionsEndsForHttp10() + public function testRequestStreamReturnsResponseBodyUntilConnectionsEndsForHttp10() { $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base . 'stream/1'), $this->loop); @@ -473,21 +473,47 @@ public function testReceiveStreamUntilConnectionsEndsForHttp10() $this->assertStringEndsWith('}', (string) $response->getBody()); } - public function testReceiveStreamChunkedForHttp11() + public function testRequestStreamReturnsResponseWithTransferEncodingChunkedAndResponseBodyDecodedForHttp11() { - $response = Block\await($this->browser->request('GET', $this->base . 'stream/1'), $this->loop); + $response = Block\await($this->browser->get($this->base . 'stream/1'), $this->loop); $this->assertEquals('1.1', $response->getProtocolVersion()); - // underlying http-client automatically decodes and doesn't expose header - // @link https://github.com/reactphp/http-client/pull/58 - // $this->assertEquals('chunked', $response->getHeaderLine('Transfer-Encoding')); - $this->assertFalse($response->hasHeader('Transfer-Encoding')); + $this->assertEquals('chunked', $response->getHeaderLine('Transfer-Encoding')); $this->assertStringStartsWith('{', (string) $response->getBody()); $this->assertStringEndsWith('}', (string) $response->getBody()); } + public function testRequestStreamWithHeadRequestReturnsEmptyResponseBodWithTransferEncodingChunkedForHttp11() + { + $response = Block\await($this->browser->head($this->base . 'stream/1'), $this->loop); + + $this->assertEquals('1.1', $response->getProtocolVersion()); + + $this->assertEquals('chunked', $response->getHeaderLine('Transfer-Encoding')); + $this->assertEquals('', (string) $response->getBody()); + } + + public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenResponseHasDoubleTransferEncoding() + { + $socket = new \React\Socket\Server(0, $this->loop); + $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) { + $connection->on('data', function () use ($connection) { + $connection->end("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked, chunked\r\nConnection: close\r\n\r\nhello"); + }); + }); + + $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; + + $response = Block\await($this->browser->get($this->base . 'stream/1'), $this->loop); + + $this->assertEquals('1.1', $response->getProtocolVersion()); + + $this->assertEquals('chunked, chunked', $response->getHeaderLine('Transfer-Encoding')); + $this->assertEquals('hello', (string) $response->getBody()); + } + public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeepsConnectionOpen() { $closed = new \React\Promise\Deferred(); From a58996a7673dd05739ca3a26c37e53084012c6e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 4 Aug 2020 13:49:29 +0200 Subject: [PATCH 038/152] Minor documentation improvements --- README.md | 10 +++++----- src/Browser.php | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 78709bba..e3532555 100644 --- a/README.md +++ b/README.md @@ -2185,13 +2185,13 @@ given timeout value applied. #### withFollowRedirects() -The `withTimeout(bool|int $$followRedirects): Browser` method can be used to +The `withTimeout(bool|int $followRedirects): Browser` method can be used to change how HTTP redirects will be followed. You can pass in the maximum number of redirects to follow: ```php -$new = $browser->withFollowRedirects(5); +$browser = $browser->withFollowRedirects(5); ``` The request will automatically be rejected when the number of redirects @@ -2326,9 +2326,9 @@ If you want to explicitly use the legacy HTTP/1.0 protocol version, you can use this method: ```php -$newBrowser = $browser->withProtocolVersion('1.0'); +$browser = $browser->withProtocolVersion('1.0'); -$newBrowser->get($url)->then(…); +$browser->get($url)->then(…); ``` Notice that the [`Browser`](#browser) is an immutable object, i.e. this @@ -2337,7 +2337,7 @@ new protocol version applied. #### withResponseBuffer() -The `withRespomseBuffer(int $maximumSize): Browser` method can be used to +The `withResponseBuffer(int $maximumSize): Browser` method can be used to change the maximum size for buffering a response body. The preferred way to send an HTTP request is by using the above diff --git a/src/Browser.php b/src/Browser.php index 91604994..816b86ba 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -433,7 +433,7 @@ public function withTimeout($timeout) * You can pass in the maximum number of redirects to follow: * * ```php - * $new = $browser->withFollowRedirects(5); + * $browser = $browser->withFollowRedirects(5); * ``` * * The request will automatically be rejected when the number of redirects @@ -604,9 +604,9 @@ public function withBase($baseUrl) * can use this method: * * ```php - * $newBrowser = $browser->withProtocolVersion('1.0'); + * $browser = $browser->withProtocolVersion('1.0'); * - * $newBrowser->get($url)->then(…); + * $browser->get($url)->then(…); * ``` * * Notice that the [`Browser`](#browser) is an immutable object, i.e. this From 0c980e466e30e79e776c41afd734714e82e55e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 21 Aug 2020 10:27:55 +0200 Subject: [PATCH 039/152] Remove internal Response class, use PSR-7 response instead --- src/Client/Request.php | 66 +------- src/Client/Response.php | 173 --------------------- src/Io/Sender.php | 23 +-- tests/Client/FunctionalIntegrationTest.php | 19 +-- tests/Client/RequestTest.php | 168 ++------------------ tests/Client/ResponseTest.php | 103 ------------ 6 files changed, 36 insertions(+), 516 deletions(-) delete mode 100644 src/Client/Response.php delete mode 100644 tests/Client/ResponseTest.php diff --git a/src/Client/Request.php b/src/Client/Request.php index 7ebb627f..51e03313 100644 --- a/src/Client/Request.php +++ b/src/Client/Request.php @@ -138,7 +138,8 @@ public function handleData($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 { - list($response, $bodyChunk) = $this->parseResponse($this->buffer); + $response = gPsr\parse_response($this->buffer); + $bodyChunk = (string) $response->getBody(); } catch (\InvalidArgumentException $exception) { $this->emit('error', array($exception)); } @@ -155,17 +156,9 @@ public function handleData($data) return; } - $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 - )); - }); + $this->stream->on('close', array($this, 'handleClose')); - $this->emit('response', array($response, $this)); + $this->emit('response', array($response, $this->stream)); $this->stream->emit('data', array($bodyChunk)); } @@ -222,30 +215,6 @@ public function close() $this->removeAllListeners(); } - protected function parseResponse($data) - { - $psrResponse = gPsr\parse_response($data); - $headers = array_map(function($val) { - if (1 === count($val)) { - $val = $val[0]; - } - - return $val; - }, $psrResponse->getHeaders()); - - $factory = $this->getResponseFactory(); - - $response = $factory( - 'HTTP', - $psrResponse->getProtocolVersion(), - $psrResponse->getStatusCode(), - $psrResponse->getReasonPhrase(), - $headers - ); - - return array($response, (string)($psrResponse->getBody())); - } - protected function connect() { $scheme = $this->requestData->getScheme(); @@ -265,31 +234,4 @@ protected function connect() return $this->connector ->connect($host . ':' . $port); } - - public function setResponseFactory($factory) - { - $this->responseFactory = $factory; - } - - public function getResponseFactory() - { - if (null === $factory = $this->responseFactory) { - $stream = $this->stream; - - $factory = function ($protocol, $version, $code, $reasonPhrase, $headers) use ($stream) { - return new Response( - $stream, - $protocol, - $version, - $code, - $reasonPhrase, - $headers - ); - }; - - $this->responseFactory = $factory; - } - - return $factory; - } } diff --git a/src/Client/Response.php b/src/Client/Response.php deleted file mode 100644 index 132e0a54..00000000 --- a/src/Client/Response.php +++ /dev/null @@ -1,173 +0,0 @@ -stream = $stream; - $this->protocol = $protocol; - $this->version = $version; - $this->code = $code; - $this->reasonPhrase = $reasonPhrase; - $this->headers = $headers; - - $this->stream->on('data', array($this, 'handleData')); - $this->stream->on('error', array($this, 'handleError')); - $this->stream->on('end', array($this, 'handleEnd')); - $this->stream->on('close', array($this, 'handleClose')); - } - - public function getProtocol() - { - return $this->protocol; - } - - public function getVersion() - { - return $this->version; - } - - public function getCode() - { - return $this->code; - } - - public function getReasonPhrase() - { - return $this->reasonPhrase; - } - - public function getHeaders() - { - return $this->headers; - } - - private function getHeader($name) - { - $name = strtolower($name); - $normalized = array_change_key_case($this->headers, CASE_LOWER); - - return isset($normalized[$name]) ? (array)$normalized[$name] : array(); - } - - /** - * @param string $name - * @return string - */ - public function getHeaderLine($name) - { - return implode(', ' , $this->getHeader($name)); - } - - /** - * @param string $name - * @return bool - */ - public function hasHeader($name) - { - return $this->getHeader($name) !== array(); - } - - /** @internal */ - public function handleData($data) - { - if ($this->readable) { - $this->emit('data', array($data)); - } - } - - /** @internal */ - public function handleEnd() - { - if (!$this->readable) { - return; - } - $this->emit('end'); - $this->close(); - } - - /** @internal */ - public function handleError(\Exception $error) - { - if (!$this->readable) { - return; - } - $this->emit('error', array(new \RuntimeException( - "An error occurred in the underlying stream", - 0, - $error - ))); - - $this->close(); - } - - /** @internal */ - public function handleClose() - { - $this->close(); - } - - public function close() - { - if (!$this->readable) { - return; - } - - $this->readable = false; - $this->stream->close(); - - $this->emit('close'); - $this->removeAllListeners(); - } - - public function isReadable() - { - return $this->readable; - } - - public function pause() - { - if (!$this->readable) { - return; - } - - $this->stream->pause(); - } - - public function resume() - { - if (!$this->readable) { - return; - } - - $this->stream->resume(); - } - - public function pipe(WritableStreamInterface $dest, array $options = array()) - { - Util::pipe($this, $dest, $options); - - return $dest; - } -} diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 353fa536..1eb098c6 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -3,10 +3,9 @@ namespace React\Http\Io; use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; use React\EventLoop\LoopInterface; use React\Http\Client\Client as HttpClient; -use React\Http\Client\Response as ResponseStream; -use React\Http\Message\Response; use React\Promise\PromiseInterface; use React\Promise\Deferred; use React\Socket\ConnectorInterface; @@ -108,26 +107,18 @@ public function send(RequestInterface $request) $deferred->reject($error); }); - $requestStream->on('response', function (ResponseStream $responseStream) use ($deferred, $request) { + $requestStream->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred, $request) { $length = null; - $body = $responseStream; - $code = $responseStream->getCode(); + $code = $response->getStatusCode(); if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code == 204 || $code == 304) { $length = 0; - } elseif (\strtolower($responseStream->getHeaderLine('Transfer-Encoding')) === 'chunked') { + } elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') { $body = new ChunkedDecoder($body); - } elseif ($responseStream->hasHeader('Content-Length')) { - $length = (int) $responseStream->getHeaderLine('Content-Length'); + } elseif ($response->hasHeader('Content-Length')) { + $length = (int) $response->getHeaderLine('Content-Length'); } - // apply response header values from response stream - $deferred->resolve(new Response( - $responseStream->getCode(), - $responseStream->getHeaders(), - new ReadableBodyStream($body, $length), - $responseStream->getVersion(), - $responseStream->getReasonPhrase() - )); + $deferred->resolve($response->withBody(new ReadableBodyStream($body, $length))); }); if ($body instanceof ReadableStreamInterface) { diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index db82b1f1..57861f2c 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -3,13 +3,14 @@ namespace React\Tests\Http\Client; use Clue\React\Block; +use Psr\Http\Message\ResponseInterface; use React\EventLoop\Factory; use React\Http\Client\Client; -use React\Http\Client\Response; use React\Promise\Deferred; use React\Promise\Stream; use React\Socket\Server; use React\Socket\ConnectionInterface; +use React\Stream\ReadableStreamInterface; use React\Tests\Http\TestCase; class FunctionalIntegrationTest extends TestCase @@ -69,8 +70,8 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp $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); + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) { + $body->on('data', $once); }); $promise = Stream\first($request, 'close'); @@ -88,8 +89,8 @@ public function testSuccessfulResponseEmitsEnd() $request = $client->request('GET', 'http://www.google.com/'); $once = $this->expectCallableOnce(); - $request->on('response', function (Response $response) use ($once) { - $response->on('end', $once); + $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) { + $body->on('end', $once); }); $promise = Stream\first($request, 'close'); @@ -112,8 +113,8 @@ public function testPostDataReturnsData() $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('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred) { + $deferred->resolve(Stream\buffer($body)); }); $request->on('error', 'printf'); @@ -145,8 +146,8 @@ public function testPostJsonReturnsData() $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('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred) { + $deferred->resolve(Stream\buffer($body)); }); $request->on('error', 'printf'); diff --git a/tests/Client/RequestTest.php b/tests/Client/RequestTest.php index e702d315..207aa15f 100644 --- a/tests/Client/RequestTest.php +++ b/tests/Client/RequestTest.php @@ -26,10 +26,6 @@ public function setUpStream() $this->connector = $this->getMockBuilder('React\Socket\ConnectorInterface') ->getMock(); - - $this->response = $this->getMockBuilder('React\Http\Client\Response') - ->disableOriginalConstructor() - ->getMock(); } /** @test */ @@ -81,48 +77,13 @@ public function requestShouldBindToStreamEventsAndUseconnector() ->method('removeListener') ->with('close', $this->identicalTo(array($request, 'handleClose'))); - $response = $this->response; - - $this->stream->expects($this->once()) - ->method('emit') - ->with('data', $this->identicalTo(array('body'))); - - $response->expects($this->at(0)) - ->method('on') - ->with('close', $this->anything()) - ->will($this->returnCallback(function ($event, $cb) use (&$endCallback) { - $endCallback = $cb; - })); - - $factory = $this->createCallableMock(); - $factory->expects($this->once()) - ->method('__invoke') - ->with('HTTP', '1.0', '200', 'OK', array('Content-Type' => 'text/plain')) - ->will($this->returnValue($response)); - - $request->setResponseFactory($factory); - - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke') - ->with($response); - - $request->on('response', $handler); $request->on('end', $this->expectCallableNever()); - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke'); - - $request->on('close', $handler); $request->end(); $request->handleData("HTTP/1.0 200 OK\r\n"); $request->handleData("Content-Type: text/plain\r\n"); $request->handleData("\r\nbody"); - - $this->assertNotNull($endCallback); - call_user_func($endCallback); } /** @test */ @@ -209,7 +170,7 @@ public function requestShouldEmitErrorIfConnectionEmitsError() } /** @test */ - public function requestShouldEmitErrorIfGuzzleParseThrowsException() + public function requestShouldEmitErrorIfRequestParserThrowsException() { $requestData = new RequestData('GET', 'http://www.example.com/'); $request = new Request($this->connector, $requestData); @@ -288,12 +249,6 @@ public function postRequestShouldSendAPostRequest() ->method('write') ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome post data$#")); - $factory = $this->createCallableMock(); - $factory->expects($this->once()) - ->method('__invoke') - ->will($this->returnValue($this->response)); - - $request->setResponseFactory($factory); $request->end('some post data'); $request->handleData("HTTP/1.0 200 OK\r\n"); @@ -322,13 +277,6 @@ public function writeWithAPostRequestShouldSendToTheStream() ->method('write') ->with($this->identicalTo("data")); - $factory = $this->createCallableMock(); - $factory->expects($this->once()) - ->method('__invoke') - ->will($this->returnValue($this->response)); - - $request->setResponseFactory($factory); - $request->write("some"); $request->write("post"); $request->end("data"); @@ -356,13 +304,6 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent ->method('write') ->with($this->identicalTo("data")); - $factory = $this->createCallableMock(); - $factory->expects($this->once()) - ->method('__invoke') - ->will($this->returnValue($this->response)); - - $request->setResponseFactory($factory); - $this->assertFalse($request->write("some")); $this->assertFalse($request->write("post")); @@ -402,13 +343,6 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB ->method('write') ->with($this->identicalTo("data")); - $factory = $this->createCallableMock(); - $factory->expects($this->once()) - ->method('__invoke') - ->will($this->returnValue($this->response)); - - $request->setResponseFactory($factory); - $this->assertFalse($request->write("some")); $this->assertFalse($request->write("post")); @@ -447,17 +381,10 @@ public function pipeShouldPipeDataIntoTheRequestBody() ->method('write') ->with($this->identicalTo("data")); - $factory = $this->createCallableMock(); - $factory->expects($this->once()) - ->method('__invoke') - ->will($this->returnValue($this->response)); - $loop = $this ->getMockBuilder('React\EventLoop\LoopInterface') ->getMock(); - $request->setResponseFactory($factory); - $stream = fopen('php://memory', 'r+'); $stream = new DuplexResourceStream($stream, $loop); @@ -572,43 +499,6 @@ public function closeShouldCancelPendingConnectionAttempt() $request->close(); } - /** @test */ - public function requestShouldRelayErrorEventsFromResponse() - { - $requestData = new RequestData('GET', 'http://www.example.com/'); - $request = new Request($this->connector, $requestData); - - $this->successfulConnectionMock(); - - $response = $this->response; - - $response->expects($this->at(0)) - ->method('on') - ->with('close', $this->anything()); - $response->expects($this->at(1)) - ->method('on') - ->with('error', $this->anything()) - ->will($this->returnCallback(function ($event, $cb) use (&$errorCallback) { - $errorCallback = $cb; - })); - - $factory = $this->createCallableMock(); - $factory->expects($this->once()) - ->method('__invoke') - ->with('HTTP', '1.0', '200', 'OK', array('Content-Type' => 'text/plain')) - ->will($this->returnValue($response)); - - $request->setResponseFactory($factory); - $request->end(); - - $request->handleData("HTTP/1.0 200 OK\r\n"); - $request->handleData("Content-Type: text/plain\r\n"); - $request->handleData("\r\nbody"); - - $this->assertNotNull($errorCallback); - call_user_func($errorCallback, new \Exception('test')); - } - /** @test */ public function requestShouldRemoveAllListenerAfterClosed() { @@ -660,25 +550,12 @@ public function multivalueHeader() $this->successfulConnectionMock(); - $response = $this->response; - - $response->expects($this->at(0)) - ->method('on') - ->with('close', $this->anything()); - $response->expects($this->at(1)) - ->method('on') - ->with('error', $this->anything()) - ->will($this->returnCallback(function ($event, $cb) use (&$errorCallback) { - $errorCallback = $cb; - })); - - $factory = $this->createCallableMock(); - $factory->expects($this->once()) - ->method('__invoke') - ->with('HTTP', '1.0', '200', 'OK', array('Content-Type' => 'text/plain', 'X-Xss-Protection' => '1; mode=block', 'Cache-Control' => 'public, must-revalidate, max-age=0')) - ->will($this->returnValue($response)); - - $request->setResponseFactory($factory); + $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"); @@ -687,28 +564,13 @@ public function multivalueHeader() $request->handleData("Cache-Control:public, must-revalidate, max-age=0\r\n"); $request->handleData("\r\nbody"); - $this->assertNotNull($errorCallback); - call_user_func($errorCallback, new \Exception('test')); - } - - /** @test */ - public function chunkedStreamDecoder() - { - $requestData = new RequestData('GET', 'http://www.example.com/'); - $request = new Request($this->connector, $requestData); - - $this->successfulConnectionMock(); - - $request->end(); - - $this->stream->expects($this->once()) - ->method('emit') - ->with('data', array("1\r\nb\r")); - - $request->handleData("HTTP/1.0 200 OK\r\n"); - $request->handleData("Transfer-Encoding: chunked\r\n"); - $request->handleData("\r\n1\r\nb\r"); - $request->handleData("\n3\t\nody\r\n0\t\n\r\n"); - + /** @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/Client/ResponseTest.php b/tests/Client/ResponseTest.php deleted file mode 100644 index 0e757619..00000000 --- a/tests/Client/ResponseTest.php +++ /dev/null @@ -1,103 +0,0 @@ -stream = $this->getMockBuilder('React\Stream\DuplexStreamInterface') - ->getMock(); - } - - /** @test */ - public function responseShouldEmitEndEventOnEnd() - { - $this->stream - ->expects($this->at(0)) - ->method('on') - ->with('data', $this->anything()); - $this->stream - ->expects($this->at(1)) - ->method('on') - ->with('error', $this->anything()); - $this->stream - ->expects($this->at(2)) - ->method('on') - ->with('end', $this->anything()); - $this->stream - ->expects($this->at(3)) - ->method('on') - ->with('close', $this->anything()); - - $response = new Response($this->stream, 'HTTP', '1.0', '200', 'OK', array('Content-Type' => 'text/plain')); - - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke') - ->with('some data'); - - $response->on('data', $handler); - - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke'); - - $response->on('end', $handler); - - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke'); - - $response->on('close', $handler); - - $this->stream - ->expects($this->at(0)) - ->method('close'); - - $response->handleData('some data'); - $response->handleEnd(); - - $this->assertSame( - array( - 'Content-Type' => 'text/plain' - ), - $response->getHeaders() - ); - } - - /** @test */ - public function closedResponseShouldNotBeResumedOrPaused() - { - $response = new Response($this->stream, 'http', '1.0', '200', 'ok', array('content-type' => 'text/plain')); - - $this->stream - ->expects($this->never()) - ->method('pause'); - $this->stream - ->expects($this->never()) - ->method('resume'); - - $response->handleEnd(); - - $response->resume(); - $response->pause(); - - $this->assertSame( - array( - 'content-type' => 'text/plain', - ), - $response->getHeaders() - ); - } -} - From f584fa42c463ec6b1dcf932de95fa94fc5c0a119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 21 Aug 2020 15:19:06 +0200 Subject: [PATCH 040/152] Clean up tests --- tests/Client/RequestTest.php | 96 ++++++++-------------------------- tests/FunctionalServerTest.php | 12 ++--- 2 files changed, 29 insertions(+), 79 deletions(-) diff --git a/tests/Client/RequestTest.php b/tests/Client/RequestTest.php index 207aa15f..d7e43c3a 100644 --- a/tests/Client/RequestTest.php +++ b/tests/Client/RequestTest.php @@ -86,28 +86,30 @@ public function requestShouldBindToStreamEventsAndUseconnector() $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->rejectedConnectionMock(); - - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke') - ->with( - $this->isInstanceOf('RuntimeException') - ); - - $request->on('error', $handler); + $this->connector->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException())); - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke'); + $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); - $request->on('close', $handler); + $request->on('close', $this->expectCallableOnce()); $request->on('end', $this->expectCallableNever()); $request->end(); @@ -121,20 +123,9 @@ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() $this->successfulConnectionMock(); - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke') - ->with( - $this->isInstanceOf('RuntimeException') - ); - - $request->on('error', $handler); - - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke'); + $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); - $request->on('close', $handler); + $request->on('close', $this->expectCallableOnce()); $request->on('end', $this->expectCallableNever()); $request->end(); @@ -149,20 +140,9 @@ public function requestShouldEmitErrorIfConnectionEmitsError() $this->successfulConnectionMock(); - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke') - ->with( - $this->isInstanceOf('Exception') - ); - - $request->on('error', $handler); + $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('Exception'))); - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke'); - - $request->on('close', $handler); + $request->on('close', $this->expectCallableOnce()); $request->on('end', $this->expectCallableNever()); $request->end(); @@ -177,14 +157,7 @@ public function requestShouldEmitErrorIfRequestParserThrowsException() $this->successfulConnectionMock(); - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke') - ->with( - $this->isInstanceOf('\InvalidArgumentException') - ); - - $request->on('error', $handler); + $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); $request->end(); $request->handleData("\r\n\r\n"); @@ -198,14 +171,7 @@ public function requestShouldEmitErrorIfUrlIsInvalid() $requestData = new RequestData('GET', 'ftp://www.example.com'); $request = new Request($this->connector, $requestData); - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke') - ->with( - $this->isInstanceOf('\InvalidArgumentException') - ); - - $request->on('error', $handler); + $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); $this->connector->expects($this->never()) ->method('connect'); @@ -221,14 +187,7 @@ public function requestShouldEmitErrorIfUrlHasNoScheme() $requestData = new RequestData('GET', 'www.example.com'); $request = new Request($this->connector, $requestData); - $handler = $this->createCallableMock(); - $handler->expects($this->once()) - ->method('__invoke') - ->with( - $this->isInstanceOf('\InvalidArgumentException') - ); - - $request->on('error', $handler); + $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); $this->connector->expects($this->never()) ->method('connect'); @@ -533,15 +492,6 @@ private function successfulAsyncConnectionMock() }; } - private function rejectedConnectionMock() - { - $this->connector - ->expects($this->once()) - ->method('connect') - ->with('www.example.com:80') - ->will($this->returnValue(new RejectedPromise(new \RuntimeException()))); - } - /** @test */ public function multivalueHeader() { diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index 3355b732..bd127ab7 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -300,8 +300,8 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); } $loop = Factory::create(); @@ -339,8 +339,8 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); } $loop = Factory::create(); @@ -408,8 +408,8 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); } $loop = Factory::create(); From db00f68c84a9553692ddbfa1814695c95ab8ee75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 29 Aug 2020 13:21:21 +0200 Subject: [PATCH 041/152] Update to reactphp/socket v1.6 (support PHP 8) --- composer.json | 4 ++-- examples/99-server-benchmark-download.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index e7739754..4f4d4aa8 100644 --- a/composer.json +++ b/composer.json @@ -32,8 +32,8 @@ "react/event-loop": "^1.0 || ^0.5", "react/promise": "^2.3 || ^1.2.1", "react/promise-stream": "^1.1", - "react/socket": "^1.1", - "react/stream": "^1.0 || ^0.7.5", + "react/socket": "^1.6", + "react/stream": "^1.1", "ringcentral/psr7": "^1.2" }, "require-dev": { diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index 536f4515..5fdd55c9 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -119,7 +119,7 @@ public function getSize() ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop, array('tcp' => array('backlog' => 511))); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; From cb90a6f3a7b533cecd954eeb59e97e5204e4924f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 30 Aug 2020 21:18:21 +0200 Subject: [PATCH 042/152] Fix checking chunked size exceeding integer bounds (PHP 8) --- src/Io/ChunkedDecoder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Io/ChunkedDecoder.php b/src/Io/ChunkedDecoder.php index 45642a63..2f58f42b 100644 --- a/src/Io/ChunkedDecoder.php +++ b/src/Io/ChunkedDecoder.php @@ -123,7 +123,7 @@ public function handleData($data) } $this->chunkSize = @\hexdec($hexValue); - if (\dechex($this->chunkSize) !== $hexValue) { + if (!\is_int($this->chunkSize) || \dechex($this->chunkSize) !== $hexValue) { $this->handleError(new Exception($hexValue . ' is not a valid hexadecimal number')); return; } From 4b182a9f564d2a422fb08ef45d9564e37b9f40c7 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Mon, 31 Aug 2020 15:14:32 +0200 Subject: [PATCH 043/152] Update PHPUnit configuration schema for PHPUnit 9.3 --- .gitattributes | 1 + .travis.yml | 7 ++++--- composer.json | 2 +- phpunit.xml.dist | 16 ++++++++++------ phpunit.xml.legacy | 18 ++++++++++++++++++ 5 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 phpunit.xml.legacy diff --git a/.gitattributes b/.gitattributes index f2f51ddf..64ab6e0f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,4 +3,5 @@ /.travis.yml export-ignore /examples export-ignore /phpunit.xml.dist export-ignore +/phpunit.xml.legacy export-ignore /tests export-ignore diff --git a/.travis.yml b/.travis.yml index d3de1e55..3ccbd4c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: php # lock distro so new future defaults will not break the build dist: trusty -matrix: +jobs: include: - php: 5.3 dist: precise @@ -23,9 +23,10 @@ matrix: - php: hhvm-3.18 install: - - composer install --no-interaction + - composer install - if [ "$DEPENDENCIES" = "lowest" ]; then composer update --prefer-lowest -n; fi script: - - ./vendor/bin/phpunit --coverage-text + - if [[ "$TRAVIS_PHP_VERSION" > "7.2" ]]; then vendor/bin/phpunit --coverage-text; fi + - if [[ "$TRAVIS_PHP_VERSION" < "7.3" ]]; then vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy; fi - if [ "$DEPENDENCIES" = "lowest" ]; then php -n tests/benchmark-middleware-runner.php; fi diff --git a/composer.json b/composer.json index 4f4d4aa8..6924ebff 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,7 @@ "clue/http-proxy-react": "^1.3", "clue/reactphp-ssh-proxy": "^1.0", "clue/socks-react": "^1.0", - "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" }, "autoload": { "psr-4": { "React\\Http\\": "src" } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0e947b87..fa88e7e0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,15 +1,19 @@ - + + ./tests/ - - - + + ./src/ - - + + diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy new file mode 100644 index 00000000..fbb43e85 --- /dev/null +++ b/phpunit.xml.legacy @@ -0,0 +1,18 @@ + + + + + + + ./tests/ + + + + + ./src/ + + + From 7658f54072d1ca65020f70ccc1627f803063bd1b Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Wed, 2 Sep 2020 09:30:27 +0200 Subject: [PATCH 044/152] Replace deprecated at() Mocks --- tests/Client/RequestTest.php | 119 +++++++++++------------------------ tests/Io/TransactionTest.php | 94 +++++++++++++++++---------- tests/TestCase.php | 26 ++++++-- 3 files changed, 120 insertions(+), 119 deletions(-) diff --git a/tests/Client/RequestTest.php b/tests/Client/RequestTest.php index d7e43c3a..fb2dc884 100644 --- a/tests/Client/RequestTest.php +++ b/tests/Client/RequestTest.php @@ -36,46 +36,21 @@ public function requestShouldBindToStreamEventsAndUseconnector() $this->successfulConnectionMock(); - $this->stream - ->expects($this->at(0)) - ->method('on') - ->with('drain', $this->identicalTo(array($request, 'handleDrain'))); - $this->stream - ->expects($this->at(1)) - ->method('on') - ->with('data', $this->identicalTo(array($request, 'handleData'))); - $this->stream - ->expects($this->at(2)) - ->method('on') - ->with('end', $this->identicalTo(array($request, 'handleEnd'))); - $this->stream - ->expects($this->at(3)) - ->method('on') - ->with('error', $this->identicalTo(array($request, 'handleError'))); - $this->stream - ->expects($this->at(4)) - ->method('on') - ->with('close', $this->identicalTo(array($request, 'handleClose'))); - $this->stream - ->expects($this->at(6)) - ->method('removeListener') - ->with('drain', $this->identicalTo(array($request, 'handleDrain'))); - $this->stream - ->expects($this->at(7)) - ->method('removeListener') - ->with('data', $this->identicalTo(array($request, 'handleData'))); - $this->stream - ->expects($this->at(8)) - ->method('removeListener') - ->with('end', $this->identicalTo(array($request, 'handleEnd'))); - $this->stream - ->expects($this->at(9)) - ->method('removeListener') - ->with('error', $this->identicalTo(array($request, 'handleError'))); - $this->stream - ->expects($this->at(10)) - ->method('removeListener') - ->with('close', $this->identicalTo(array($request, 'handleClose'))); + $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()); @@ -223,18 +198,11 @@ public function writeWithAPostRequestShouldSendToTheStream() $this->successfulConnectionMock(); - $this->stream - ->expects($this->at(5)) - ->method('write') - ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome$#")); - $this->stream - ->expects($this->at(6)) - ->method('write') - ->with($this->identicalTo("post")); - $this->stream - ->expects($this->at(7)) - ->method('write') - ->with($this->identicalTo("data")); + $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"); @@ -253,15 +221,12 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent $resolveConnection = $this->successfulAsyncConnectionMock(); - $this->stream - ->expects($this->at(5)) - ->method('write') - ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsomepost$#")) - ->willReturn(true); - $this->stream - ->expects($this->at(6)) - ->method('write') - ->with($this->identicalTo("data")); + $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")); @@ -292,15 +257,12 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB $resolveConnection = $this->successfulAsyncConnectionMock(); - $this->stream - ->expects($this->at(0)) - ->method('write') - ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsomepost$#")) - ->willReturn(false); - $this->stream - ->expects($this->at(1)) - ->method('write') - ->with($this->identicalTo("data")); + $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")); @@ -327,18 +289,11 @@ public function pipeShouldPipeDataIntoTheRequestBody() $this->successfulConnectionMock(); - $this->stream - ->expects($this->at(5)) - ->method('write') - ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\nsome$#")); - $this->stream - ->expects($this->at(6)) - ->method('write') - ->with($this->identicalTo("post")); - $this->stream - ->expects($this->at(7)) - ->method('write') - ->with($this->identicalTo("data")); + $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') diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 384a74a6..12e128cc 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -523,18 +523,21 @@ public function testFollowingRedirectWithSpecifiedHeaders() // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithUserAgent $redirectResponse = new Response(301, array('Location' => 'http://redirect.com/')); - $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithUserAgent $okResponse = new Response(200); $that = $this; - $sender->expects($this->at(1)) - ->method('send') - ->with($this->callback(function (RequestInterface $request) use ($that) { + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($this->anything()), + array($this->callback(function (RequestInterface $request) use ($that) { $that->assertEquals(array('Chrome'), $request->getHeader('User-Agent')); return true; - }))->willReturn(Promise\resolve($okResponse)); + })) + )->willReturnOnConsecutiveCalls( + Promise\resolve($redirectResponse), + Promise\resolve($okResponse) + ); $transaction = new Transaction($sender, $loop); $transaction->send($requestWithUserAgent); @@ -551,18 +554,21 @@ public function testRemovingAuthorizationHeaderWhenChangingHostnamesDuringRedire // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithAuthorization $redirectResponse = new Response(301, array('Location' => 'http://redirect.com/')); - $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithAuthorization $okResponse = new Response(200); $that = $this; - $sender->expects($this->at(1)) - ->method('send') - ->with($this->callback(function (RequestInterface $request) use ($that) { + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($this->anything()), + array($this->callback(function (RequestInterface $request) use ($that) { $that->assertFalse($request->hasHeader('Authorization')); return true; - }))->willReturn(Promise\resolve($okResponse)); + })) + )->willReturnOnConsecutiveCalls( + Promise\resolve($redirectResponse), + Promise\resolve($okResponse) + ); $transaction = new Transaction($sender, $loop); $transaction->send($requestWithAuthorization); @@ -579,18 +585,21 @@ public function testAuthorizationHeaderIsForwardedWhenRedirectingToSameDomain() // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithAuthorization $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); - $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithAuthorization $okResponse = new Response(200); $that = $this; - $sender->expects($this->at(1)) - ->method('send') - ->with($this->callback(function (RequestInterface $request) use ($that) { + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($this->anything()), + array($this->callback(function (RequestInterface $request) use ($that) { $that->assertEquals(array('secret'), $request->getHeader('Authorization')); return true; - }))->willReturn(Promise\resolve($okResponse)); + })) + )->willReturnOnConsecutiveCalls( + Promise\resolve($redirectResponse), + Promise\resolve($okResponse) + ); $transaction = new Transaction($sender, $loop); $transaction->send($requestWithAuthorization); @@ -606,19 +615,22 @@ public function testAuthorizationHeaderIsForwardedWhenLocationContainsAuthentica // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithAuthorization $redirectResponse = new Response(301, array('Location' => 'http://user:pass@example.com/new')); - $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithAuthorization $okResponse = new Response(200); $that = $this; - $sender->expects($this->at(1)) - ->method('send') - ->with($this->callback(function (RequestInterface $request) use ($that) { + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($this->anything()), + array($this->callback(function (RequestInterface $request) use ($that) { $that->assertEquals('user:pass', $request->getUri()->getUserInfo()); $that->assertFalse($request->hasHeader('Authorization')); return true; - }))->willReturn(Promise\resolve($okResponse)); + })) + )->willReturnOnConsecutiveCalls( + Promise\resolve($redirectResponse), + Promise\resolve($okResponse) + ); $transaction = new Transaction($sender, $loop); $transaction->send($request); @@ -639,19 +651,22 @@ public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting() // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithCustomHeaders $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); - $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithCustomHeaders $okResponse = new Response(200); $that = $this; - $sender->expects($this->at(1)) - ->method('send') - ->with($this->callback(function (RequestInterface $request) use ($that) { + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($this->anything()), + array($this->callback(function (RequestInterface $request) use ($that) { $that->assertFalse($request->hasHeader('Content-Type')); $that->assertFalse($request->hasHeader('Content-Length')); - return true; - }))->willReturn(Promise\resolve($okResponse)); + return true;; + })) + )->willReturnOnConsecutiveCalls( + Promise\resolve($redirectResponse), + Promise\resolve($okResponse) + ); $transaction = new Transaction($sender, $loop); $transaction->send($requestWithCustomHeaders); @@ -706,12 +721,17 @@ public function testCancelTransactionWillCancelRedirectedRequest() // mock sender to resolve promise with the given $redirectResponse in $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); - $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); // mock sender to return pending promise which should be cancelled when cancelling result - $sender->expects($this->at(1))->method('send')->willReturn($pending); + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($this->anything()), + array($this->anything()) + )->willReturnOnConsecutiveCalls( + Promise\resolve($redirectResponse), + $pending + ); $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); @@ -728,12 +748,17 @@ public function testCancelTransactionWillCancelRedirectedRequestAgain() // mock sender to resolve promise with the given $redirectResponse in $first = new Deferred(); - $sender->expects($this->at(0))->method('send')->willReturn($first->promise()); $second = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); // mock sender to return pending promise which should be cancelled when cancelling result - $sender->expects($this->at(1))->method('send')->willReturn($second); + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($this->anything()), + array($this->anything()) + )->willReturnOnConsecutiveCalls( + $first->promise(), + $second + ); $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); @@ -794,12 +819,17 @@ public function testCancelTransactionShouldCancelSendingPromise() // mock sender to resolve promise with the given $redirectResponse in $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); - $sender->expects($this->at(0))->method('send')->willReturn(Promise\resolve($redirectResponse)); $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); // mock sender to return pending promise which should be cancelled when cancelling result - $sender->expects($this->at(1))->method('send')->willReturn($pending); + $sender->expects($this->exactly(2))->method('send')->withConsecutive( + array($this->anything()), + array($this->anything()) + )->willReturnOnConsecutiveCalls( + Promise\resolve($redirectResponse), + $pending + ); $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); diff --git a/tests/TestCase.php b/tests/TestCase.php index 575ac274..a905c324 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -41,13 +41,29 @@ protected function expectCallableConsecutive($numberOfCalls, array $with) { $mock = $this->createCallableMock(); - for ($i = 0; $i < $numberOfCalls; $i++) { - $mock - ->expects($this->at($i)) - ->method('__invoke') - ->with($this->equalTo($with[$i])); + if($numberOfCalls == 2){ + $mock->expects($this->exactly($numberOfCalls))->method('__invoke')->withConsecutive( + array($this->equalTo($with[0])), + array($this->equalTo($with[1])) + ); } + if($numberOfCalls == 3){ + $mock->expects($this->exactly($numberOfCalls))->method('__invoke')->withConsecutive( + array($this->equalTo($with[0])), + array($this->equalTo($with[1])), + array($this->equalTo($with[2])) + ); + } + + if($numberOfCalls == 4){ + $mock->expects($this->exactly($numberOfCalls))->method('__invoke')->withConsecutive( + array($this->equalTo($with[0])), + array($this->equalTo($with[1])), + array($this->equalTo($with[2])), + array($this->equalTo($with[3])) + ); + } return $mock; } From c1cbd3bf006e82776eb7b538f2b590d27ea10005 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Thu, 3 Sep 2020 13:17:03 +0200 Subject: [PATCH 045/152] Refactor tests --- tests/Io/ChunkedDecoderTest.php | 69 ++++++++++++++++++++++++++++----- tests/TestCase.php | 30 -------------- 2 files changed, 59 insertions(+), 40 deletions(-) diff --git a/tests/Io/ChunkedDecoderTest.php b/tests/Io/ChunkedDecoderTest.php index b78a4cdd..822ceaa6 100644 --- a/tests/Io/ChunkedDecoderTest.php +++ b/tests/Io/ChunkedDecoderTest.php @@ -8,6 +8,9 @@ class ChunkedDecoderTest extends TestCase { + private $input; + private $parser; + /** * @before */ @@ -28,11 +31,17 @@ public function testSimpleChunk() public function testTwoChunks() { - $this->parser->on('data', $this->expectCallableConsecutive(2, array('hello', 'bla'))); + $buffer = array(); + $this->parser->on('data', function ($data) use (&$buffer) { + $buffer[] = $data; + }); + $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableNever()); $this->input->emit('data', array("5\r\nhello\r\n3\r\nbla\r\n")); + + $this->assertEquals(array('hello', 'bla'), $buffer); } public function testEnd() @@ -46,12 +55,18 @@ public function testEnd() public function testParameterWithEnd() { - $this->parser->on('data', $this->expectCallableConsecutive(2, array('hello', 'bla'))); + $buffer = array(); + $this->parser->on('data', function ($data) use (&$buffer) { + $buffer[] = $data; + }); + $this->parser->on('end', $this->expectCallableOnce()); $this->parser->on('close', $this->expectCallableOnce()); $this->parser->on('error', $this->expectCallableNever()); $this->input->emit('data', array("5\r\nhello\r\n3\r\nbla\r\n0\r\n\r\n")); + + $this->assertEquals(array('hello', 'bla'), $buffer); } public function testInvalidChunk() @@ -118,7 +133,11 @@ public function testSplittedBoth() public function testCompletlySplitted() { - $this->parser->on('data', $this->expectCallableConsecutive(2, array('we', 'lt'))); + $buffer = array(); + $this->parser->on('data', function ($data) use (&$buffer) { + $buffer[] = $data; + }); + $this->parser->on('close', $this->expectCallableNever()); $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); @@ -127,25 +146,36 @@ public function testCompletlySplitted() $this->input->emit('data', array("\r\n")); $this->input->emit('data', array("we")); $this->input->emit('data', array("lt\r\n")); + + $this->assertEquals(array('we', 'lt'), $buffer); } public function testMixed() { - $this->parser->on('data', $this->expectCallableConsecutive(3, array('we', 'lt', 'hello'))); + $buffer = array(); + $this->parser->on('data', function ($data) use (&$buffer) { + $buffer[] = $data; + }); + $this->parser->on('close', $this->expectCallableNever()); $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); $this->input->emit('data', array("4")); $this->input->emit('data', array("\r\n")); - $this->input->emit('data', array("we")); - $this->input->emit('data', array("lt\r\n")); + $this->input->emit('data', array("welt\r\n")); $this->input->emit('data', array("5\r\nhello\r\n")); + + $this->assertEquals(array('welt', 'hello'), $buffer); } public function testBigger() { - $this->parser->on('data', $this->expectCallableConsecutive(2, array('abcdeabcdeabcdea', 'hello'))); + $buffer = array(); + $this->parser->on('data', function ($data) use (&$buffer) { + $buffer[] = $data; + }); + $this->parser->on('close', $this->expectCallableNever()); $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); @@ -155,11 +185,17 @@ public function testBigger() $this->input->emit('data', array("\r\n")); $this->input->emit('data', array("abcdeabcdeabcdea\r\n")); $this->input->emit('data', array("5\r\nhello\r\n")); + + $this->assertEquals(array('abcdeabcdeabcdea', 'hello'), $buffer); } public function testOneUnfinished() { - $this->parser->on('data', $this->expectCallableConsecutive(2, array('bla', 'hello'))); + $buffer = array(); + $this->parser->on('data', function ($data) use (&$buffer) { + $buffer[] = $data; + }); + $this->parser->on('close', $this->expectCallableNever()); $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); @@ -167,6 +203,8 @@ public function testOneUnfinished() $this->input->emit('data', array("3\r\n")); $this->input->emit('data', array("bla\r\n")); $this->input->emit('data', array("5\r\nhello")); + + $this->assertEquals(array('bla', 'hello'), $buffer); } public function testChunkIsBiggerThenExpected() @@ -326,7 +364,10 @@ public function testHexDecimalInBodyIsPotentialThreadSplitted() public function testEmitSingleCharacter() { - $this->parser->on('data', $this->expectCallableConsecutive(4, array('t', 'e', 's', 't'))); + $buffer = array(); + $this->parser->on('data', function ($data) use (&$buffer) { + $buffer[] = $data; + }); $this->parser->on('close', $this->expectCallableOnce()); $this->parser->on('end', $this->expectCallableOnce()); $this->parser->on('error', $this->expectCallableNever()); @@ -336,6 +377,8 @@ public function testEmitSingleCharacter() foreach ($array as $character) { $this->input->emit('data', array($character)); } + + $this->assertEquals(array('t', 'e', 's', 't'), $buffer); } public function testHandleError() @@ -402,13 +445,19 @@ public function testOutputStreamCanCloseInputStream() public function testLeadingZerosWillBeIgnored() { - $this->parser->on('data', $this->expectCallableConsecutive(2, array('hello', 'hello world'))); + $buffer = array(); + $this->parser->on('data', function ($data) use (&$buffer) { + $buffer[] = $data; + }); + $this->parser->on('error', $this->expectCallableNever()); $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableNever()); $this->input->emit('data', array("00005\r\nhello\r\n")); $this->input->emit('data', array("0000b\r\nhello world\r\n")); + + $this->assertEquals(array('hello', 'hello world'), $buffer); } public function testLeadingZerosInEndChunkWillBeIgnored() diff --git a/tests/TestCase.php b/tests/TestCase.php index a905c324..1938ed89 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -37,36 +37,6 @@ protected function expectCallableNever() return $mock; } - protected function expectCallableConsecutive($numberOfCalls, array $with) - { - $mock = $this->createCallableMock(); - - if($numberOfCalls == 2){ - $mock->expects($this->exactly($numberOfCalls))->method('__invoke')->withConsecutive( - array($this->equalTo($with[0])), - array($this->equalTo($with[1])) - ); - } - - if($numberOfCalls == 3){ - $mock->expects($this->exactly($numberOfCalls))->method('__invoke')->withConsecutive( - array($this->equalTo($with[0])), - array($this->equalTo($with[1])), - array($this->equalTo($with[2])) - ); - } - - if($numberOfCalls == 4){ - $mock->expects($this->exactly($numberOfCalls))->method('__invoke')->withConsecutive( - array($this->equalTo($with[0])), - array($this->equalTo($with[1])), - array($this->equalTo($with[2])), - array($this->equalTo($with[3])) - ); - } - return $mock; - } - protected function createCallableMock() { if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { From 754b0c18545d258922ffa907f3b18598280fdecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 11 Sep 2020 13:01:51 +0200 Subject: [PATCH 046/152] Prepare v1.1.0 release --- CHANGELOG.md | 19 ++++++++++++++++++- README.md | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aadbfa7c..8718c68d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 1.1.0 (2020-09-11) + +* Feature: Support upcoming PHP 8 release, update to reactphp/socket v1.6 and adjust type checks for invalid chunk headers. + (#391 by @clue) + +* Feature: Consistently resolve base URL according to HTTP specs. + (#379 by @clue) + +* Feature / Fix: Expose `Transfer-Encoding: chunked` response header and fix chunked responses for `HEAD` requests. + (#381 by @clue) + +* Internal refactoring to remove unneeded `MessageFactory` and `Response` classes. + (#380 and #389 by @clue) + +* Minor documentation improvements and improve test suite, update to support PHPUnit 9.3. + (#385 by @clue and #393 by @SimonFrings) + ## 1.0.0 (2020-07-11) A major new feature release, see [**release announcement**](https://clue.engineering/2020/announcing-reactphp-http). @@ -84,7 +101,7 @@ minutes. See below for more details: This improves default concurrency to 1024 requests and caps the default request buffer at 64K. The previous defaults resulted in just 4 concurrent requests with a request buffer of 8M. - See [`Server`](../README.md#server) for details on how to override these defaults. + See [`Server`](README.md#server) for details on how to override these defaults. * Feature: Expose ReactPHP in `User-Agent` client-side request header and in `Server` server-side response header. (#374 by @clue) diff --git a/README.md b/README.md index e3532555..a1061b6a 100644 --- a/README.md +++ b/README.md @@ -2732,7 +2732,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require react/http:^1.0 +$ composer require react/http:^1.1 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From a19a34dad90a1fa728e8233a07675f0c6d780ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 13 Sep 2020 13:20:39 +0200 Subject: [PATCH 047/152] Add BufferedBody PSR-7 message body implementation --- src/Io/BufferedBody.php | 176 ++++++++++++++++++++ tests/Io/BufferedBodyTest.php | 300 ++++++++++++++++++++++++++++++++++ 2 files changed, 476 insertions(+) create mode 100644 src/Io/BufferedBody.php create mode 100644 tests/Io/BufferedBodyTest.php diff --git a/src/Io/BufferedBody.php b/src/Io/BufferedBody.php new file mode 100644 index 00000000..2f81bc56 --- /dev/null +++ b/src/Io/BufferedBody.php @@ -0,0 +1,176 @@ +buffer = $buffer; + } + + public function __toString() + { + if ($this->closed) { + return ''; + } + + $this->seek(0); + + return $this->getContents(); + } + + public function close() + { + $this->buffer = ''; + $this->position = 0; + $this->closed = true; + } + + public function detach() + { + $this->close(); + + return null; + } + + public function getSize() + { + return $this->closed ? null : \strlen($this->buffer); + } + + public function tell() + { + if ($this->closed) { + throw new \RuntimeException('Unable to tell position of closed stream'); + } + + return $this->position; + } + + public function eof() + { + return $this->position >= \strlen($this->buffer); + } + + public function isSeekable() + { + return !$this->closed; + } + + public function seek($offset, $whence = \SEEK_SET) + { + if ($this->closed) { + throw new \RuntimeException('Unable to seek on closed stream'); + } + + $old = $this->position; + + if ($whence === \SEEK_SET) { + $this->position = $offset; + } elseif ($whence === \SEEK_CUR) { + $this->position += $offset; + } elseif ($whence === \SEEK_END) { + $this->position = \strlen($this->buffer) + $offset; + } else { + throw new \InvalidArgumentException('Invalid seek mode given'); + } + + if (!\is_int($this->position) || $this->position < 0) { + $this->position = $old; + throw new \RuntimeException('Unable to seek to position'); + } + } + + public function rewind() + { + $this->seek(0); + } + + public function isWritable() + { + return !$this->closed; + } + + public function write($string) + { + if ($this->closed) { + throw new \RuntimeException('Unable to write to closed stream'); + } + + if ($string === '') { + return 0; + } + + if ($this->position > 0 && !isset($this->buffer[$this->position - 1])) { + $this->buffer = \str_pad($this->buffer, $this->position, "\0"); + } + + $len = \strlen($string); + $this->buffer = \substr($this->buffer, 0, $this->position) . $string . \substr($this->buffer, $this->position + $len); + $this->position += $len; + + return $len; + } + + public function isReadable() + { + return !$this->closed; + } + + public function read($length) + { + if ($this->closed) { + throw new \RuntimeException('Unable to read from closed stream'); + } + + if ($length < 1) { + throw new \InvalidArgumentException('Invalid read length given'); + } + + if ($this->position + $length > \strlen($this->buffer)) { + $length = \strlen($this->buffer) - $this->position; + } + + if (!isset($this->buffer[$this->position])) { + return ''; + } + + $pos = $this->position; + $this->position += $length; + + return \substr($this->buffer, $pos, $length); + } + + public function getContents() + { + if ($this->closed) { + throw new \RuntimeException('Unable to read from closed stream'); + } + + if (!isset($this->buffer[$this->position])) { + return ''; + } + + $pos = $this->position; + $this->position = \strlen($this->buffer); + + return \substr($this->buffer, $pos); + } + + public function getMetadata($key = null) + { + return $key === null ? array() : null; + } +} diff --git a/tests/Io/BufferedBodyTest.php b/tests/Io/BufferedBodyTest.php new file mode 100644 index 00000000..01154e71 --- /dev/null +++ b/tests/Io/BufferedBodyTest.php @@ -0,0 +1,300 @@ +assertTrue($stream->isReadable()); + $this->assertTrue($stream->isWritable()); + $this->assertTrue($stream->isSeekable()); + $this->assertSame(0, $stream->getSize()); + $this->assertSame('', $stream->getContents()); + $this->assertSame('', (string) $stream); + } + + public function testClose() + { + $stream = new BufferedBody('hello'); + $stream->close(); + + $this->assertFalse($stream->isReadable()); + $this->assertFalse($stream->isWritable()); + $this->assertFalse($stream->isSeekable()); + $this->assertTrue($stream->eof()); + $this->assertNull($stream->getSize()); + $this->assertSame('', (string) $stream); + } + + public function testDetachReturnsNullAndCloses() + { + $stream = new BufferedBody('hello'); + $this->assertNull($stream->detach()); + + $this->assertFalse($stream->isReadable()); + $this->assertFalse($stream->isWritable()); + $this->assertFalse($stream->isSeekable()); + $this->assertTrue($stream->eof()); + $this->assertNull($stream->getSize()); + $this->assertSame('', (string) $stream); + } + + public function testSeekAndTellPosition() + { + $stream = new BufferedBody('hello'); + + $this->assertSame(0, $stream->tell()); + $this->assertFalse($stream->eof()); + + $stream->seek(1); + $this->assertSame(1, $stream->tell()); + $this->assertFalse($stream->eof()); + + $stream->seek(2, SEEK_CUR); + $this->assertSame(3, $stream->tell()); + $this->assertFalse($stream->eof()); + + $stream->seek(-1, SEEK_END); + $this->assertSame(4, $stream->tell()); + $this->assertFalse($stream->eof()); + + $stream->seek(0, SEEK_END); + $this->assertSame(5, $stream->tell()); + $this->assertTrue($stream->eof()); + } + + public function testSeekAfterEndIsPermitted() + { + $stream = new BufferedBody('hello'); + + $stream->seek(1000); + $this->assertSame(1000, $stream->tell()); + $this->assertTrue($stream->eof()); + + $stream->seek(0, SEEK_END); + $this->assertSame(5, $stream->tell()); + $this->assertTrue($stream->eof()); + } + + public function testSeekBeforeStartThrows() + { + $stream = new BufferedBody('hello'); + + try { + $stream->seek(-10, SEEK_CUR); + } catch (\RuntimeException $e) { + $this->assertSame(0, $stream->tell()); + + $this->setExpectedException('RuntimeException'); + throw $e; + } + } + + public function testSeekWithInvalidModeThrows() + { + $stream = new BufferedBody('hello'); + + $this->setExpectedException('InvalidArgumentException'); + $stream->seek(1, 12345); + } + + public function testSeekAfterCloseThrows() + { + $stream = new BufferedBody('hello'); + $stream->close(); + + $this->setExpectedException('RuntimeException'); + $stream->seek(0); + } + + public function testTellAfterCloseThrows() + { + $stream = new BufferedBody('hello'); + $stream->close(); + + $this->setExpectedException('RuntimeException'); + $stream->tell(); + } + + public function testRewindSeeksToStartPosition() + { + $stream = new BufferedBody('hello'); + + $stream->seek(1); + $stream->rewind(); + $this->assertSame(0, $stream->tell()); + } + + public function testRewindAfterCloseThrows() + { + $stream = new BufferedBody('hello'); + $stream->close(); + + $this->setExpectedException('RuntimeException'); + $stream->rewind(); + } + + public function testGetContentsMultipleTimesReturnsBodyOnlyOnce() + { + $stream = new BufferedBody('hello'); + + $this->assertSame(5, $stream->getSize()); + $this->assertSame('hello', $stream->getContents()); + $this->assertSame('', $stream->getContents()); + } + + public function testReadReturnsChunkAndAdvancesPosition() + { + $stream = new BufferedBody('hello'); + + $this->assertSame('he', $stream->read(2)); + $this->assertSame(2, $stream->tell()); + + $this->assertSame('ll', $stream->read(2)); + $this->assertSame(4, $stream->tell()); + + $this->assertSame('o', $stream->read(2)); + $this->assertSame(5, $stream->tell()); + + $this->assertSame('', $stream->read(2)); + $this->assertSame(5, $stream->tell()); + } + + public function testReadAfterEndReturnsEmptyStringWithoutChangingPosition() + { + $stream = new BufferedBody('hello'); + + $stream->seek(1000); + + $this->assertSame('', $stream->read(2)); + $this->assertSame(1000, $stream->tell()); + } + + public function testReadZeroThrows() + { + $stream = new BufferedBody('hello'); + + $this->setExpectedException('InvalidArgumentException'); + $stream->read(0); + } + + public function testReadAfterCloseThrows() + { + $stream = new BufferedBody('hello'); + $stream->close(); + + $this->setExpectedException('RuntimeException'); + $stream->read(10); + } + + public function testGetContentsReturnsWholeBufferAndAdvancesPositionToEof() + { + $stream = new BufferedBody('hello'); + + $this->assertSame('hello', $stream->getContents()); + $this->assertSame(5, $stream->tell()); + $this->assertTrue($stream->eof()); + } + + public function testGetContentsAfterEndsReturnsEmptyStringWithoutChangingPosition() + { + $stream = new BufferedBody('hello'); + + $stream->seek(100); + + $this->assertSame('', $stream->getContents()); + $this->assertSame(100, $stream->tell()); + $this->assertTrue($stream->eof()); + } + + public function testGetContentsAfterCloseThrows() + { + $stream = new BufferedBody('hello'); + $stream->close(); + + $this->setExpectedException('RuntimeException'); + $stream->getContents(); + } + + public function testWriteAdvancesPosition() + { + $stream = new BufferedBody(''); + + $this->assertSame(2, $stream->write('he')); + $this->assertSame(2, $stream->tell()); + + $this->assertSame(2, $stream->write('ll')); + $this->assertSame(4, $stream->tell()); + + $this->assertSame(1, $stream->write('o')); + $this->assertSame(5, $stream->tell()); + + $this->assertSame(0, $stream->write('')); + $this->assertSame(5, $stream->tell()); + } + + public function testWriteInMiddleOfBufferOverwrites() + { + $stream = new BufferedBody('hello'); + + $stream->seek(1); + $this->assertSame(1, $stream->write('a')); + + $this->assertSame(2, $stream->tell()); + $this->assertsame(5, $stream->getSize()); + $this->assertSame('hallo', (string) $stream); + } + + public function testWriteOverEndOverwritesAndAppends() + { + $stream = new BufferedBody('hello'); + + $stream->seek(4); + $this->assertSame(2, $stream->write('au')); + + $this->assertSame(6, $stream->tell()); + $this->assertsame(6, $stream->getSize()); + $this->assertSame('hellau', (string) $stream); + } + + public function testWriteAfterEndAppendsAndFillsWithNullBytes() + { + $stream = new BufferedBody('hello'); + + $stream->seek(6); + $this->assertSame(6, $stream->write('binary')); + + $this->assertSame(12, $stream->tell()); + $this->assertsame(12, $stream->getSize()); + $this->assertSame('hello' . "\0" . 'binary', (string) $stream); + } + + public function testWriteAfterCloseThrows() + { + $stream = new BufferedBody('hello'); + $stream->close(); + + $this->setExpectedException('RuntimeException'); + $stream->write('foo'); + } + + public function testGetMetadataWithoutKeyReturnsEmptyArray() + { + $stream = new BufferedBody('hello'); + + $this->assertEquals(array(), $stream->getMetadata()); + } + + public function testGetMetadataWithKeyReturnsNull() + { + $stream = new BufferedBody('hello'); + + $this->assertNull($stream->getMetadata('key')); + } +} From 6fec25b0d317a496e7669708192a09dd6d403eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 14 Sep 2020 17:00:54 +0200 Subject: [PATCH 048/152] Keep request body in memory also after consuming request body --- .../RequestBodyBufferMiddleware.php | 8 +-- tests/ServerTest.php | 71 +++++++++++++++++-- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/Middleware/RequestBodyBufferMiddleware.php b/src/Middleware/RequestBodyBufferMiddleware.php index 0e6f5145..c13a5dec 100644 --- a/src/Middleware/RequestBodyBufferMiddleware.php +++ b/src/Middleware/RequestBodyBufferMiddleware.php @@ -4,10 +4,10 @@ use OverflowException; use Psr\Http\Message\ServerRequestInterface; +use React\Http\Io\BufferedBody; use React\Http\Io\IniUtil; use React\Promise\Stream; use React\Stream\ReadableStreamInterface; -use RingCentral\Psr7\BufferStream; final class RequestBodyBufferMiddleware { @@ -38,7 +38,7 @@ public function __invoke(ServerRequestInterface $request, $stack) if ($size === 0 || !$body instanceof ReadableStreamInterface) { // replace with empty body if body is streaming (or buffered size exceeds limit) if ($body instanceof ReadableStreamInterface || $size > $this->sizeLimit) { - $request = $request->withBody(new BufferStream(0)); + $request = $request->withBody(new BufferedBody('')); } return $stack($request); @@ -51,9 +51,7 @@ public function __invoke(ServerRequestInterface $request, $stack) } return Stream\buffer($body, $sizeLimit)->then(function ($buffer) use ($request, $stack) { - $stream = new BufferStream(\strlen($buffer)); - $stream->write($buffer); - $request = $request->withBody($stream); + $request = $request->withBody(new BufferedBody($buffer)); return $stack($request); }, function ($error) use ($stack, $request, $body) { diff --git a/tests/ServerTest.php b/tests/ServerTest.php index ce19dda9..84d93eb7 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -110,6 +110,35 @@ function (ServerRequestInterface $request) use (&$called) { $this->assertSame('beforeokafter', $called); } + public function testPostFormData() + { + $loop = Factory::create(); + $deferred = new Deferred(); + $server = new Server($loop, function (ServerRequestInterface $request) use ($deferred) { + $deferred->resolve($request); + }); + + $server->listen($this->socket); + $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(), $loop); + assert($request instanceof ServerRequestInterface); + + $form = $request->getParsedBody(); + + $this->assertTrue(isset($form['foo'])); + $this->assertEquals('bar', $form['foo']); + + $this->assertEquals(array(), $request->getUploadedFiles()); + + $body = $request->getBody(); + + $this->assertSame(7, $body->getSize()); + $this->assertSame(7, $body->tell()); + $this->assertSame('foo=bar', (string) $body); + } + public function testPostFileUpload() { $loop = Factory::create(); @@ -132,11 +161,14 @@ public function testPostFileUpload() } }); - $parsedRequest = Block\await($deferred->promise(), $loop); - $this->assertNotEmpty($parsedRequest->getUploadedFiles()); - $this->assertEmpty($parsedRequest->getParsedBody()); + $request = Block\await($deferred->promise(), $loop); + assert($request instanceof ServerRequestInterface); + + $this->assertEmpty($request->getParsedBody()); + + $this->assertNotEmpty($request->getUploadedFiles()); - $files = $parsedRequest->getUploadedFiles(); + $files = $request->getUploadedFiles(); $this->assertTrue(isset($files['file'])); $this->assertCount(1, $files); @@ -144,6 +176,37 @@ public function testPostFileUpload() $this->assertSame('hello.txt', $files['file']->getClientFilename()); $this->assertSame('text/plain', $files['file']->getClientMediaType()); $this->assertSame("hello\r\n", (string)$files['file']->getStream()); + + $body = $request->getBody(); + + $this->assertSame(220, $body->getSize()); + $this->assertSame(220, $body->tell()); + } + + public function testPostJsonWillNotBeParsedByDefault() + { + $loop = Factory::create(); + $deferred = new Deferred(); + $server = new Server($loop, function (ServerRequestInterface $request) use ($deferred) { + $deferred->resolve($request); + }); + + $server->listen($this->socket); + $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(), $loop); + assert($request instanceof ServerRequestInterface); + + $this->assertNull($request->getParsedBody()); + + $this->assertSame(array(), $request->getUploadedFiles()); + + $body = $request->getBody(); + + $this->assertSame(6, $body->getSize()); + $this->assertSame(0, $body->tell()); + $this->assertSame('[true]', (string) $body); } public function testServerReceivesBufferedRequestByDefault() From badb0a87890e14b9cdfa3aec3ba1aafd900401ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 4 Dec 2020 13:57:33 +0100 Subject: [PATCH 049/152] Prepare v1.2.0 release --- CHANGELOG.md | 9 +++++++++ README.md | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8718c68d..9734f11a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 1.2.0 (2020-12-04) + +* Feature: Keep request body in memory also after consuming request body. + (#395 by @clue) + + This means consumers can now always access the complete request body as + detailed in the documentation. This allows building custom parsers and more + advanced processing models without having to mess with the default parsers. + ## 1.1.0 (2020-09-11) * Feature: Support upcoming PHP 8 release, update to reactphp/socket v1.6 and adjust type checks for invalid chunk headers. diff --git a/README.md b/README.md index a1061b6a..9b50acd9 100644 --- a/README.md +++ b/README.md @@ -287,7 +287,7 @@ header. If the server requires authentication, if may return a `401` (Unauthoriz status code which will reject the request by default (see also the [`withRejectErrorResponse()` method](#withrejecterrorresponse) below). -In order to pass authentication details, you can simple pass the username and +In order to pass authentication details, you can simply pass the username and password as part of the request URL like this: ```php @@ -2185,7 +2185,7 @@ given timeout value applied. #### withFollowRedirects() -The `withTimeout(bool|int $followRedirects): Browser` method can be used to +The `withFollowRedirects(bool|int $followRedirects): Browser` method can be used to change how HTTP redirects will be followed. You can pass in the maximum number of redirects to follow: @@ -2732,7 +2732,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require react/http:^1.1 +$ composer require react/http:^1.2 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 8dc1e4e4be1c679267d7916f6f13323bc074b4ac Mon Sep 17 00:00:00 2001 From: Paul Vogel Date: Wed, 6 Jan 2021 17:52:39 +0100 Subject: [PATCH 050/152] Fix some typos, a mixed up param name and add forgotten namespace backslash --- src/Browser.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Browser.php b/src/Browser.php index 816b86ba..188320e8 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -15,7 +15,7 @@ use InvalidArgumentException; /** - * @final This class is final and shouldn't be extended as it is likely to be marked final in a future relase. + * @final This class is final and shouldn't be extended as it is likely to be marked final in a future release. */ class Browser { @@ -302,7 +302,7 @@ public function delete($url, array $headers = array(), $contents = '') * @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 = '') { @@ -373,11 +373,11 @@ 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(), $contents = '') + public function requestStreaming($method, $url, $headers = array(), $body = '') { - return $this->withOptions(array('streaming' => true))->requestMayBeStreaming($method, $url, $headers, $contents); + return $this->withOptions(array('streaming' => true))->requestMayBeStreaming($method, $url, $headers, $body); } /** @@ -720,7 +720,7 @@ 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 = '') { From 161550b2d2fd1a6da90c0b299f0f52b105f33d89 Mon Sep 17 00:00:00 2001 From: Paul Vogel Date: Wed, 6 Jan 2021 18:47:26 +0100 Subject: [PATCH 051/152] Update example 63 to support v1.0.0+ Add LoopInterface as first constructor argument to Server and change Server to accept variadic middleware handlers instead of array. --- examples/63-server-streaming-request.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index 9c1a9758..45eb0dea 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -9,7 +9,8 @@ // Note how this example uses the advanced `StreamingRequestMiddleware` to allow streaming // the incoming HTTP request. This very simple example merely counts the size // of the streaming body, it does not otherwise buffer its contents in memory. -$server = new React\Http\Server(array( +$server = new React\Http\Server( + $loop, new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $body = $request->getBody(); @@ -44,7 +45,7 @@ function (Psr\Http\Message\ServerRequestInterface $request) { }); }); } -)); +); $server->on('error', 'printf'); From 8fba44a2908599899e0cc6c435dce6b5d3594abb Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Wed, 3 Feb 2021 11:57:36 +0100 Subject: [PATCH 052/152] Use GitHub actions for continuous integration (CI) Bye bye Travis CI, you've served us well. --- .gitattributes | 2 +- .github/workflows/ci.yml | 45 ++++++++++++++++++++++++++++++++++++++++ .travis.yml | 32 ---------------------------- README.md | 2 +- 4 files changed, 47 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml diff --git a/.gitattributes b/.gitattributes index 64ab6e0f..f658344c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,6 @@ /.gitattributes export-ignore +/.github/ export-ignore /.gitignore export-ignore -/.travis.yml export-ignore /examples export-ignore /phpunit.xml.dist export-ignore /phpunit.xml.legacy export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..d321f381 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + pull_request: + +jobs: + PHPUnit: + name: PHPUnit (PHP ${{ matrix.php }}) + runs-on: ubuntu-20.04 + strategy: + matrix: + php: + - 7.4 + - 7.3 + - 7.2 + - 7.1 + - 7.0 + - 5.6 + - 5.5 + - 5.4 + - 5.3 + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: xdebug + - run: composer install + - run: vendor/bin/phpunit --coverage-text + if: ${{ matrix.php >= 7.3 }} + - run: vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy + if: ${{ matrix.php < 7.3 }} + + PHPUnit-hhvm: + name: PHPUnit (HHVM) + runs-on: ubuntu-18.04 + continue-on-error: true + steps: + - uses: actions/checkout@v2 + - uses: azjezz/setup-hhvm@v1 + with: + version: lts-3.30 + - run: hhvm $(which composer) install + - run: hhvm vendor/bin/phpunit diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3ccbd4c1..00000000 --- a/.travis.yml +++ /dev/null @@ -1,32 +0,0 @@ -language: php - -# lock distro so new future defaults will not break the build -dist: trusty - -jobs: - include: - - php: 5.3 - dist: precise - - php: 5.4 - - php: 5.5 - - php: 5.6 - - php: 7.0 - - php: 7.0 - env: - - DEPENDENCIES=lowest - - php: 7.1 - - php: 7.2 - - php: 7.3 - - php: 7.4 - - php: hhvm-3.18 - allow_failures: - - php: hhvm-3.18 - -install: - - composer install - - if [ "$DEPENDENCIES" = "lowest" ]; then composer update --prefer-lowest -n; fi - -script: - - if [[ "$TRAVIS_PHP_VERSION" > "7.2" ]]; then vendor/bin/phpunit --coverage-text; fi - - if [[ "$TRAVIS_PHP_VERSION" < "7.3" ]]; then vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy; fi - - if [ "$DEPENDENCIES" = "lowest" ]; then php -n tests/benchmark-middleware-runner.php; fi diff --git a/README.md b/README.md index 9b50acd9..2f4867c9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # HTTP -[![Build Status](https://travis-ci.org/reactphp/http.svg?branch=master)](https://travis-ci.org/reactphp/http) +[![CI status](https://github.com/reactphp/http/workflows/CI/badge.svg)](https://github.com/reactphp/http/actions) Event-driven, streaming HTTP client and server implementation for [ReactPHP](https://reactphp.org/). From 57f4e5f8c6519549fd53056646c0b355da91f351 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Wed, 3 Feb 2021 12:21:32 +0100 Subject: [PATCH 053/152] Support PHP 8 --- .github/workflows/ci.yml | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d321f381..cf214c83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: php: + - 8.0 - 7.4 - 7.3 - 7.2 diff --git a/README.md b/README.md index 2f4867c9..19109f8c 100644 --- a/README.md +++ b/README.md @@ -2738,7 +2738,7 @@ $ composer require react/http:^1.2 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.3 through current PHP 7+ and +extensions and supports running on legacy PHP 5.3 through current PHP 8+ and HHVM. It's *highly recommended to use PHP 7+* for this project. From aaca1b3e0eb991b19c410cf7468a87d5db6d46bb Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Wed, 3 Feb 2021 12:36:34 +0100 Subject: [PATCH 054/152] Set Xdebug's stack limit to 256 for legacy PHP --- tests/Client/FunctionalIntegrationTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 57861f2c..2db75b35 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -167,6 +167,9 @@ public function testPostJsonReturnsData() /** @group internet */ 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); + $loop = Factory::create(); $client = new Client($loop); From f76e1473a2dc0810035caf216543f1024275e31c Mon Sep 17 00:00:00 2001 From: Fritz Gerneth Date: Mon, 28 Dec 2020 14:48:56 +0100 Subject: [PATCH 055/152] Fix broken anchor link in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 19109f8c..09a4ecd2 100644 --- a/README.md +++ b/README.md @@ -1846,7 +1846,7 @@ implementations and ongoing effort to standardize interfaces between these with and support this goal. As such, this project only bundles a few middleware implementations that are required to match PHP's request behavior (see -[middleware implementations](#react-http-middleware)) and otherwise actively +[middleware implementations](#reacthttpmiddleware)) and otherwise actively encourages third-party middleware implementations. While we would love to support PSR-15 directly in `react/http`, we understand From 6bcbe54d965025d4b456008604f9046b9cee420b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 24 Feb 2021 18:04:52 +0100 Subject: [PATCH 056/152] Require Host request header for HTTP/1.1 requests All HTTP/1.1 requests require a Host request header as per RFC 7230. The proxy CONNECT method is an exception to this rule because we do not want to break compatibility with common HTTP proxy clients that do not strictly follow the RFCs. This does not affect valid HTTP/1.1 requests and has no effect on HTTP/1.0 requests. Additionally, make sure we do not include a default Host request header in the parsed request object if the incoming request does not make use of the Host request header. --- src/Io/RequestHeaderParser.php | 14 ++- tests/Io/RequestHeaderParserTest.php | 14 +-- tests/Io/StreamingServerTest.php | 133 ++++++++++++++++----------- tests/ServerTest.php | 1 + 4 files changed, 96 insertions(+), 66 deletions(-) diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 53f7ff09..2c9b121c 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -172,6 +172,7 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) } // 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'; } @@ -234,8 +235,8 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) $request = $request->withRequestTarget($start['target']); } - // Optional Host header value MUST be valid (host and optional port) - if ($request->hasHeader('Host')) { + 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) @@ -248,6 +249,12 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) 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 @@ -270,9 +277,6 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) } } - // always sanitize Host header because it contains critical routing information - $request = $request->withUri($request->getUri()->withUserInfo('u')->withUserInfo('')); - return $request; } } diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index b0a339ed..356443fb 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -257,7 +257,7 @@ public function testHeaderEventWithShouldApplyDefaultAddressFromLocalConnectionA $connection->emit('data', array("GET /foo HTTP/1.0\r\n\r\n")); $this->assertEquals('http://127.1.1.1:8000/foo', $request->getUri()); - $this->assertEquals('127.1.1.1:8000', $request->getHeaderLine('Host')); + $this->assertFalse($request->hasHeader('Host')); } public function testHeaderEventViaHttpsShouldApplyHttpsSchemeFromLocalTlsConnectionAddress() @@ -550,7 +550,7 @@ public function testInvalidContentLengthRequestHeaderWillEmitError() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\nContent-Length: foo\r\n\r\n")); + $connection->emit('data', array("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: foo\r\n\r\n")); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame(400, $error->getCode()); @@ -570,7 +570,7 @@ public function testInvalidRequestWithMultipleContentLengthRequestHeadersWillEmi $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\nContent-Length: 4\r\nContent-Length: 5\r\n\r\n")); + $connection->emit('data', array("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 4\r\nContent-Length: 5\r\n\r\n")); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame(400, $error->getCode()); @@ -590,7 +590,7 @@ public function testInvalidTransferEncodingRequestHeaderWillEmitError() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\nTransfer-Encoding: foo\r\n\r\n")); + $connection->emit('data', array("GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: foo\r\n\r\n")); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame(501, $error->getCode()); @@ -610,7 +610,7 @@ public function testInvalidRequestWithBothTransferEncodingAndContentLengthWillEm $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nContent-Length: 0\r\n\r\n")); + $connection->emit('data', array("GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\nContent-Length: 0\r\n\r\n")); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame(400, $error->getCode()); @@ -762,7 +762,7 @@ public function testQueryParmetersWillBeSet() private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "\r\n"; @@ -772,7 +772,7 @@ private function createGetRequest() private function createAdvancedPostRequest() { $data = "POST /foo?bar=baz HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "User-Agent: react/alpha\r\n"; $data .= "Connection: close\r\n"; $data .= "\r\n"; diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index d2401a06..c771330b 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -210,7 +210,7 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored() $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.1\r\nHost: example.com:80\r\n\r\n"; + $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); @@ -222,6 +222,41 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored() $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); } + public function testRequestGetHttp10WithoutHostWillBeIgnored() + { + $requestAssertion = null; + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $requestAssertion = $request; + }); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.0\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); + $this->assertSame('GET', $requestAssertion->getMethod()); + $this->assertSame('/', $requestAssertion->getRequestTarget()); + $this->assertSame('/', $requestAssertion->getUri()->getPath()); + $this->assertSame('http://127.0.0.1/', (string)$requestAssertion->getUri()); + $this->assertNull($requestAssertion->getUri()->getPort()); + $this->assertEquals('1.0', $requestAssertion->getProtocolVersion()); + $this->assertSame('', $requestAssertion->getHeaderLine('Host')); + } + + public function testRequestGetHttp11WithoutHostWillReject() + { + $server = new StreamingServer(Factory::create(), 'var_dump'); + $server->on('error', $this->expectCallableOnce()); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $data = "GET / HTTP/1.1\r\n\r\n"; + $this->connection->emit('data', array($data)); + } + public function testRequestOptionsAsterisk() { $requestAssertion = null; @@ -277,7 +312,7 @@ public function testRequestConnectAuthorityForm() $this->assertSame('example.com:443', $requestAssertion->getHeaderLine('Host')); } - public function testRequestConnectWithoutHostWillBeAdded() + public function testRequestConnectWithoutHostWillBePassesAsIs() { $requestAssertion = null; $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { @@ -296,10 +331,10 @@ public function testRequestConnectWithoutHostWillBeAdded() $this->assertSame('', $requestAssertion->getUri()->getPath()); $this->assertSame('http://example.com:443/', (string)$requestAssertion->getUri()); $this->assertSame(443, $requestAssertion->getUri()->getPort()); - $this->assertSame('example.com:443', $requestAssertion->getHeaderLine('Host')); + $this->assertFalse($requestAssertion->hasHeader('Host')); } - public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() + public function testRequestConnectAuthorityFormWithDefaultPortWillBePassedAsIs() { $requestAssertion = null; $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { @@ -318,10 +353,10 @@ public function testRequestConnectAuthorityFormWithDefaultPortWillBeIgnored() $this->assertSame('', $requestAssertion->getUri()->getPath()); $this->assertSame('http://example.com/', (string)$requestAssertion->getUri()); $this->assertNull($requestAssertion->getUri()->getPort()); - $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); + $this->assertSame('example.com:80', $requestAssertion->getHeaderLine('Host')); } - public function testRequestConnectAuthorityFormNonMatchingHostWillBeOverwritten() + public function testRequestConnectAuthorityFormNonMatchingHostWillBePassedAsIs() { $requestAssertion = null; $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { @@ -340,7 +375,7 @@ public function testRequestConnectAuthorityFormNonMatchingHostWillBeOverwritten( $this->assertSame('', $requestAssertion->getUri()->getPath()); $this->assertSame('http://example.com/', (string)$requestAssertion->getUri()); $this->assertNull($requestAssertion->getUri()->getPort()); - $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); + $this->assertSame('other.example.org', $requestAssertion->getHeaderLine('Host')); } public function testRequestConnectOriginFormRequestTargetWillReject() @@ -415,7 +450,7 @@ public function testRequestAbsoluteEvent() $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); } - public function testRequestAbsoluteAddsMissingHostEvent() + public function testRequestAbsoluteNonMatchingHostWillBePassedAsIs() { $requestAssertion = null; @@ -426,37 +461,27 @@ public function testRequestAbsoluteAddsMissingHostEvent() $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET http://example.com:8080/test HTTP/1.0\r\n\r\n"; + $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->assertSame('GET', $requestAssertion->getMethod()); - $this->assertSame('http://example.com:8080/test', $requestAssertion->getRequestTarget()); - $this->assertEquals('http://example.com:8080/test', $requestAssertion->getUri()); + $this->assertSame('http://example.com/test', $requestAssertion->getRequestTarget()); + $this->assertEquals('http://example.com/test', $requestAssertion->getUri()); $this->assertSame('/test', $requestAssertion->getUri()->getPath()); - $this->assertSame('example.com:8080', $requestAssertion->getHeaderLine('Host')); + $this->assertSame('other.example.org', $requestAssertion->getHeaderLine('Host')); } - public function testRequestAbsoluteNonMatchingHostWillBeOverwritten() + public function testRequestAbsoluteWithoutHostWillReject() { - $requestAssertion = null; - - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { - $requestAssertion = $request; - }); + $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET http://example.com/test HTTP/1.1\r\nHost: other.example.org\r\n\r\n"; + $data = "GET http://example.com:8080/test HTTP/1.1\r\n\r\n"; $this->connection->emit('data', array($data)); - - $this->assertInstanceOf('RingCentral\Psr7\Request', $requestAssertion); - $this->assertSame('GET', $requestAssertion->getMethod()); - $this->assertSame('http://example.com/test', $requestAssertion->getRequestTarget()); - $this->assertEquals('http://example.com/test', $requestAssertion->getUri()); - $this->assertSame('/test', $requestAssertion->getUri()->getPath()); - $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); } public function testRequestOptionsAsteriskEvent() @@ -515,7 +540,7 @@ public function testRequestPauseWillBeForwardedToConnection() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Content-Length: 5\r\n"; $data .= "\r\n"; @@ -535,7 +560,7 @@ public function testRequestResumeWillBeForwardedToConnection() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Content-Length: 5\r\n"; $data .= "\r\n"; @@ -954,7 +979,7 @@ function ($data) use (&$buffer) { $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.1\r\n\r\n"; + $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', array($data)); $this->assertEquals("HTTP/1.1 200 OK\r\nUpgrade: demo\r\nContent-Length: 3\r\nConnection: close\r\n\r\nfoo", $buffer); @@ -989,7 +1014,7 @@ function ($data) use (&$buffer) { $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.1\r\nUpgrade: demo\r\n\r\n"; + $data = "GET / HTTP/1.1\r\nHost: localhost\r\nUpgrade: demo\r\n\r\n"; $this->connection->emit('data', array($data)); $this->assertEquals("HTTP/1.1 200 OK\r\nContent-Length: 3\r\nConnection: close\r\n\r\nfoo", $buffer); @@ -1027,7 +1052,7 @@ function ($data) use (&$buffer) { $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.1\r\nUpgrade: demo\r\n\r\n"; + $data = "GET / HTTP/1.1\r\nHost: localhost\r\nUpgrade: demo\r\n\r\n"; $this->connection->emit('data', array($data)); $this->assertEquals("HTTP/1.1 101 Switching Protocols\r\nUpgrade: demo\r\nConnection: upgrade\r\n\r\nfoo", $buffer); @@ -1065,7 +1090,7 @@ function ($data) use (&$buffer) { $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = "GET / HTTP/1.1\r\nUpgrade: demo\r\n\r\n"; + $data = "GET / HTTP/1.1\r\nHost: localhost\r\nUpgrade: demo\r\n\r\n"; $this->connection->emit('data', array($data)); $stream->write('hello'); @@ -1417,7 +1442,7 @@ public function testRequestContentLengthBodyDataWillEmitDataEventOnRequestStream $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Content-Length: 5\r\n"; $data .= "\r\n"; @@ -1446,7 +1471,7 @@ public function testRequestChunkedTransferEncodingRequestWillEmitDecodedDataEven $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Transfer-Encoding: chunked\r\n"; $data .= "\r\n"; @@ -1476,7 +1501,7 @@ public function testRequestChunkedTransferEncodingWithAdditionalDataWontBeEmitte $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Transfer-Encoding: chunked\r\n"; $data .= "\r\n"; @@ -1505,7 +1530,7 @@ public function testRequestChunkedTransferEncodingEmpty() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Transfer-Encoding: chunked\r\n"; $data .= "\r\n"; @@ -1534,7 +1559,7 @@ public function testRequestChunkedTransferEncodingHeaderCanBeUpperCase() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Transfer-Encoding: CHUNKED\r\n"; $data .= "\r\n"; @@ -1563,7 +1588,7 @@ public function testRequestChunkedTransferEncodingCanBeMixedUpperAndLowerCase() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Transfer-Encoding: CHunKeD\r\n"; $data .= "\r\n"; @@ -1592,7 +1617,7 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Content-Length: 5\r\n"; $data .= "\r\n"; @@ -1621,7 +1646,7 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Content-Length: 5\r\n"; $data .= "\r\n"; @@ -1653,7 +1678,7 @@ public function testRequestZeroContentLengthWillEmitEndEvent() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Content-Length: 0\r\n"; $data .= "\r\n"; @@ -1679,7 +1704,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Content-Length: 0\r\n"; $data .= "\r\n"; @@ -1706,7 +1731,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Content-Length: 0\r\n"; $data .= "\r\n"; @@ -1732,7 +1757,7 @@ public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Transfer-Encoding: chunked\r\n"; $data .= "\r\n"; @@ -1756,7 +1781,7 @@ public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Transfer-Encoding: chunked\r\n"; $data .= "\r\n"; @@ -1778,7 +1803,7 @@ public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWi $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Transfer-Encoding: chunked\r\n"; $data .= "\r\n"; @@ -1801,7 +1826,7 @@ public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Transfer-Encoding: chunked\r\n"; $data .= "\r\n"; @@ -1823,7 +1848,7 @@ public function testRequestUnexpectedEndOfRequestWithContentLengthWillEmitErrorO $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Content-Length: 500\r\n"; $data .= "\r\n"; @@ -2220,7 +2245,7 @@ function ($data) use (&$buffer) { $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Expect: 100-continue\r\n"; $data .= "\r\n"; @@ -2743,7 +2768,7 @@ public function testRequestCookieWillBeAddedToServerRequest() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Cookie: hello=world\r\n"; $data .= "\r\n"; @@ -2764,7 +2789,7 @@ public function testRequestInvalidMultipleCookiesWontBeAddedToServerRequest() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Cookie: hello=world\r\n"; $data .= "Cookie: test=failed\r\n"; @@ -2785,7 +2810,7 @@ public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Cookie: hello=world; test=abc\r\n"; $data .= "\r\n"; @@ -2804,7 +2829,7 @@ public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() { $this->socket->emit('connection', array($this->connection)); $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "Cookie: test=abc,def; hello=world\r\n"; $data .= "\r\n"; @@ -2816,7 +2841,7 @@ public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() { private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; + $data .= "Host: example.com\r\n"; $data .= "Connection: close\r\n"; $data .= "\r\n"; diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 84d93eb7..ff2cd9c1 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -269,6 +269,7 @@ private function createPostFileUploadRequest() $data = array(); $data[] = "POST / HTTP/1.1\r\n"; + $data[] = "Host: localhost\r\n"; $data[] = "Content-Type: multipart/form-data; boundary=" . $boundary . "\r\n"; $data[] = "Content-Length: 220\r\n"; $data[] = "\r\n"; From 7aa08f01583c765261d08d1191f0239324f18012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 6 Apr 2018 17:33:44 +0200 Subject: [PATCH 057/152] Support persistent connections (Connection: keep-alive) --- README.md | 11 +- src/Io/StreamingServer.php | 41 +++++-- tests/FunctionalServerTest.php | 6 +- tests/Io/StreamingServerTest.php | 195 ++++++++++++++++++++++++++++++- 4 files changed, 236 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 09a4ecd2..4625a81e 100644 --- a/README.md +++ b/README.md @@ -1698,11 +1698,12 @@ so you don't have to. For instance, if the client sends the request using the HTTP/1.1 protocol version, the response message will also use the same protocol version, no matter what version is returned from the request handler function. -Note that persistent connections (`Connection: keep-alive`) are currently -not supported. -As such, HTTP/1.1 response messages will automatically include a -`Connection: close` header, irrespective of what header values are -passed explicitly. +The server supports persistent connections. An appropriate `Connection: keep-alive` +or `Connection: close` response header will be added automatically, respecting the +matching request header value and HTTP default header values. The server is +responsible for handling the `Connection` response header, so you SHOULD NOT pass +this response header yourself, unless you explicitly want to override the user's +choice with a `Connection: close` response header. ### Middleware diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 0674d960..076a4ff0 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -210,7 +210,8 @@ public function writeError(ConnectionInterface $conn, $code, ServerRequestInterf $response = new Response( $code, array( - 'Content-Type' => 'text/plain' + 'Content-Type' => 'text/plain', + 'Connection' => 'close' // we do not want to keep the connection open after an error ), 'Error ' . $code ); @@ -273,17 +274,28 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt $chunked = true; } else { // remove any Transfer-Encoding headers unless automatically enabled above + // we do not want to keep connection alive, so pretend we received "Connection: close" request header $response = $response->withoutHeader('Transfer-Encoding'); + $request = $request->withHeader('Connection', 'close'); } // assign "Connection" header automatically + $persist = false; if ($code === 101) { // 101 (Switching Protocols) response uses Connection: upgrade header + // This implies that this stream now uses another protocol and we + // may not persist this connection for additional requests. $response = $response->withHeader('Connection', 'upgrade'); - } elseif ($version === '1.1') { - // HTTP/1.1 assumes persistent connection support by default - // we do not support persistent connections, so let the client know + } elseif (\strtolower($request->getHeaderLine('Connection')) === 'close' || \strtolower($response->getHeaderLine('Connection')) === 'close') { + // obey explicit "Connection: close" request header or response header if present $response = $response->withHeader('Connection', 'close'); + } elseif ($version === '1.1') { + // HTTP/1.1 assumes persistent connection support by default, so we don't need to inform client + $persist = true; + } elseif (strtolower($request->getHeaderLine('Connection')) === 'keep-alive') { + // obey explicit "Connection: keep-alive" request header and inform client + $persist = true; + $response = $response->withHeader('Connection', 'keep-alive'); } else { // remove any Connection headers unless automatically enabled above $response = $response->withoutHeader('Connection'); @@ -328,9 +340,15 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt $body = "0\r\n\r\n"; } - // end connection after writing response headers and body + // write response headers and body $connection->write($headers . "\r\n" . $body); - $connection->end(); + + // either wait for next request over persistent connection or end connection + if ($persist) { + $this->parser->handle($connection); + } else { + $connection->end(); + } return; } @@ -345,6 +363,15 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // in particular this may only fire on a later read/write attempt. $connection->on('close', array($body, 'close')); - $body->pipe($connection); + // write streaming body and then wait for next request over persistent connection + if ($persist) { + $body->pipe($connection, array('end' => false)); + $parser = $this->parser; + $body->on('end', function () use ($connection, $parser) { + $parser->handle($connection); + }); + } else { + $body->pipe($connection); + } } } diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalServerTest.php index bd127ab7..41cf31db 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalServerTest.php @@ -662,7 +662,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() $server->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { - $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"); + $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\nConnection: close\r\n\r\n"); $conn->once('data', function () use ($conn) { $conn->write('hello'); @@ -703,7 +703,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive $server->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { - $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"); + $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\nConnection: close\r\n\r\n"); $conn->once('data', function () use ($conn) { $conn->write('hello'); @@ -737,7 +737,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() $server->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { - $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"); + $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\nConnection: close\r\n\r\n"); $conn->once('data', function () use ($conn) { $conn->write('hello'); diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index d2401a06..40187f06 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -6,6 +6,7 @@ use React\EventLoop\Factory; use React\Http\Io\StreamingServer; use React\Http\Message\Response; +use React\Http\Message\ServerRequest; use React\Promise\Promise; use React\Stream\ThroughStream; use React\Tests\Http\SocketServerStub; @@ -957,7 +958,7 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertEquals("HTTP/1.1 200 OK\r\nUpgrade: demo\r\nContent-Length: 3\r\nConnection: close\r\n\r\nfoo", $buffer); + $this->assertEquals("HTTP/1.1 200 OK\r\nUpgrade: demo\r\nContent-Length: 3\r\n\r\nfoo", $buffer); } public function testResponseUpgradeWishInRequestCanBeIgnoredByReturningNormalResponse() @@ -992,7 +993,7 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nUpgrade: demo\r\n\r\n"; $this->connection->emit('data', array($data)); - $this->assertEquals("HTTP/1.1 200 OK\r\nContent-Length: 3\r\nConnection: close\r\n\r\nfoo", $buffer); + $this->assertEquals("HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nfoo", $buffer); } public function testResponseUpgradeSwitchingProtocolIncludesConnectionUpgradeHeaderWithoutContentLength() @@ -2813,6 +2814,196 @@ public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() { $this->assertEquals(array('test' => 'abc,def', 'hello' => 'world'), $requestValidation->getCookieParams()); } + public function testNewConnectionWillInvokeParserOnce() + { + $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser->expects($this->once())->method('handle'); + + $ref = new \ReflectionProperty($server, 'parser'); + $ref->setAccessible(true); + $ref->setValue($server, $parser); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + } + + public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhenParserIsDoneForHttp10() + { + $request = new ServerRequest('GET', 'http://localhost/', array(), '', '1.0'); + + $server = new StreamingServer(Factory::create(), $this->expectCallableOnceWith($request)); + + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser->expects($this->once())->method('handle'); + + $ref = new \ReflectionProperty($server, 'parser'); + $ref->setAccessible(true); + $ref->setValue($server, $parser); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $this->connection->expects($this->once())->method('write'); + $this->connection->expects($this->once())->method('end'); + + // pretend parser just finished parsing + $server->handleRequest($this->connection, $request); + } + + public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhenParserIsDoneForHttp11ConnectionClose() + { + $request = new ServerRequest('GET', 'http://localhost/', array('Connection' => 'close')); + + $server = new StreamingServer(Factory::create(), $this->expectCallableOnceWith($request)); + + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser->expects($this->once())->method('handle'); + + $ref = new \ReflectionProperty($server, 'parser'); + $ref->setAccessible(true); + $ref->setValue($server, $parser); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $this->connection->expects($this->once())->method('write'); + $this->connection->expects($this->once())->method('end'); + + // pretend parser just finished parsing + $server->handleRequest($this->connection, $request); + } + + public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhenParserIsDoneAndRequestHandlerReturnsConnectionClose() + { + $request = new ServerRequest('GET', 'http://localhost/'); + + $server = new StreamingServer(Factory::create(), function () { + return new Response(200, array('Connection' => 'close')); + }); + + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser->expects($this->once())->method('handle'); + + $ref = new \ReflectionProperty($server, 'parser'); + $ref->setAccessible(true); + $ref->setValue($server, $parser); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $this->connection->expects($this->once())->method('write'); + $this->connection->expects($this->once())->method('end'); + + // pretend parser just finished parsing + $server->handleRequest($this->connection, $request); + } + + public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandlerWhenConnectionCanBeKeptAliveForHttp11Default() + { + $request = new ServerRequest('GET', 'http://localhost/'); + + $server = new StreamingServer(Factory::create(), function () { + return new Response(); + }); + + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser->expects($this->exactly(2))->method('handle'); + + $ref = new \ReflectionProperty($server, 'parser'); + $ref->setAccessible(true); + $ref->setValue($server, $parser); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $this->connection->expects($this->once())->method('write'); + $this->connection->expects($this->never())->method('end'); + + // pretend parser just finished parsing + $server->handleRequest($this->connection, $request); + } + + public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandlerWhenConnectionCanBeKeptAliveForHttp10ConnectionKeepAlive() + { + $request = new ServerRequest('GET', 'http://localhost/', array('Connection' => 'keep-alive'), '', '1.0'); + + $server = new StreamingServer(Factory::create(), function () { + return new Response(); + }); + + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser->expects($this->exactly(2))->method('handle'); + + $ref = new \ReflectionProperty($server, 'parser'); + $ref->setAccessible(true); + $ref->setValue($server, $parser); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $this->connection->expects($this->once())->method('write'); + $this->connection->expects($this->never())->method('end'); + + // pretend parser just finished parsing + $server->handleRequest($this->connection, $request); + } + + public function testNewConnectionWillInvokeParserOnceAfterInvokingRequestHandlerWhenStreamingResponseBodyKeepsStreaming() + { + $request = new ServerRequest('GET', 'http://localhost/'); + + $body = new ThroughStream(); + $server = new StreamingServer(Factory::create(), function () use ($body) { + return new Response(200, array(), $body); + }); + + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser->expects($this->once())->method('handle'); + + $ref = new \ReflectionProperty($server, 'parser'); + $ref->setAccessible(true); + $ref->setValue($server, $parser); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $this->connection->expects($this->once())->method('write'); + $this->connection->expects($this->never())->method('end'); + + // pretend parser just finished parsing + $server->handleRequest($this->connection, $request); + } + + public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandlerWhenStreamingResponseBodyEnds() + { + $request = new ServerRequest('GET', 'http://localhost/'); + + $body = new ThroughStream(); + $server = new StreamingServer(Factory::create(), function () use ($body) { + return new Response(200, array(), $body); + }); + + $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); + $parser->expects($this->exactly(2))->method('handle'); + + $ref = new \ReflectionProperty($server, 'parser'); + $ref->setAccessible(true); + $ref->setValue($server, $parser); + + $server->listen($this->socket); + $this->socket->emit('connection', array($this->connection)); + + $this->connection->expects($this->exactly(2))->method('write'); + $this->connection->expects($this->never())->method('end'); + + // pretend parser just finished parsing + $server->handleRequest($this->connection, $request); + + $body->end(); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n"; From 84fbe7882e0b948ae03abd67a57c5e88f2eac055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 23 Mar 2021 19:54:15 +0100 Subject: [PATCH 058/152] Improve benchmarking instructions and dangling memory references --- examples/99-server-benchmark-download.php | 13 ++++++++++--- src/Io/RequestHeaderParser.php | 4 ---- src/Io/StreamingServer.php | 3 ++- tests/Io/StreamingServerTest.php | 2 ++ 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index 5fdd55c9..1a49df72 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -1,11 +1,18 @@ /dev/null // $ wget http://localhost:8080/10g.bin -O /dev/null -// $ ab -n10 -c10 http://localhost:8080/1g.bin -// $ docker run -it --rm --net=host jordi/ab -n100000 -c10 http://localhost:8080/ -// $ docker run -it --rm --net=host jordi/ab -n10 -c10 http://localhost:8080/1g.bin +// $ ab -n10 -c10 -k http://localhost:8080/1g.bin +// $ docker run -it --rm --net=host jordi/ab -n100000 -c10 -k http://localhost:8080/ +// $ docker run -it --rm --net=host jordi/ab -n10 -c10 -k http://localhost:8080/1g.bin +// $ docker run -it --rm --net=host skandyla/wrk -t8 -c10 -d20 http://localhost:8080/ use Evenement\EventEmitter; use Psr\Http\Message\ServerRequestInterface; diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 53f7ff09..5125c77f 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -106,10 +106,6 @@ public function handle(ConnectionInterface $conn) $stream->close(); } }); - - $conn->on('close', function () use (&$buffer, &$fn) { - $fn = $buffer = null; - }); } /** diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 076a4ff0..e20ddf48 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -367,7 +367,8 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt if ($persist) { $body->pipe($connection, array('end' => false)); $parser = $this->parser; - $body->on('end', function () use ($connection, $parser) { + $body->on('end', function () use ($connection, $parser, $body) { + $connection->removeListener('close', array($body, 'close')); $parser->handle($connection); }); } else { diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 40187f06..0dde7a0c 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -3001,7 +3001,9 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle // pretend parser just finished parsing $server->handleRequest($this->connection, $request); + $this->assertCount(2, $this->connection->listeners('close')); $body->end(); + $this->assertCount(1, $this->connection->listeners('close')); } private function createGetRequest() From bc537273d11ee769c723a830e63aa33c0c35a530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 11 Apr 2021 20:07:28 +0200 Subject: [PATCH 059/152] Prepare v1.3.0 release --- CHANGELOG.md | 19 +++++++++++++++++++ README.md | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9734f11a..4ae6bc15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 1.3.0 (2021-04-11) + +* Feature: Support persistent connections (`Connection: keep-alive`). + (#405 by @clue) + + This shows a noticeable performance improvement especially when benchmarking + using persistent connections (which is the default pretty much everywhere). + Together with other changes in this release, this improves benchmarking + performance by around 100%. + +* Feature: Require `Host` request header for HTTP/1.1 requests. + (#404 by @clue) + +* Minor documentation improvements. + (#398 by @fritz-gerneth and #399 and #400 by @pavog) + +* Improve test suite, use GitHub actions for continuous integration (CI). + (#402 by @SimonFrings) + ## 1.2.0 (2020-12-04) * Feature: Keep request body in memory also after consuming request body. diff --git a/README.md b/README.md index 4625a81e..768b3f8c 100644 --- a/README.md +++ b/README.md @@ -2733,7 +2733,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require react/http:^1.2 +$ composer require react/http:^1.3 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 5417e836ec34ffb7601002b8dcad8234c73b0d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 8 Jul 2021 16:41:37 +0200 Subject: [PATCH 060/152] Simplify usage by supporting new default loop --- .github/workflows/ci.yml | 3 + README.md | 151 ++++++++---------- composer.json | 23 ++- examples/01-client-get-request.php | 5 +- examples/02-client-concurrent-requests.php | 5 +- examples/03-client-request-any.php | 5 +- examples/04-client-post-json.php | 5 +- examples/05-client-put-xml.php | 5 +- examples/11-client-http-connect-proxy.php | 13 +- examples/12-client-socks-proxy.php | 13 +- examples/13-client-ssh-proxy.php | 14 +- examples/14-client-unix-domain-sockets.php | 11 +- .../21-client-request-streaming-to-stdout.php | 9 +- .../22-client-stream-upload-from-stdin.php | 9 +- examples/51-server-hello-world.php | 9 +- examples/52-server-count-visitors.php | 9 +- examples/53-server-whatsmyip.php | 9 +- examples/54-server-query-parameter.php | 9 +- examples/55-server-cookie-handling.php | 9 +- examples/56-server-sleep.php | 14 +- examples/57-server-error-handling.php | 9 +- examples/58-server-stream-response.php | 18 +-- examples/59-server-json-api.php | 9 +- examples/61-server-hello-world-https.php | 11 +- examples/62-server-form-upload.php | 8 +- examples/63-server-streaming-request.php | 9 +- examples/71-server-http-proxy.php | 8 +- examples/72-server-http-connect-proxy.php | 10 +- examples/81-server-upgrade-echo.php | 12 +- examples/82-server-upgrade-chat.php | 12 +- examples/91-client-benchmark-download.php | 16 +- examples/92-client-benchmark-upload.php | 20 ++- examples/99-server-benchmark-download.php | 11 +- src/Browser.php | 33 ++-- .../LimitConcurrentRequestsMiddleware.php | 3 - src/Server.php | 34 ++-- tests/BrowserTest.php | 15 ++ tests/ServerTest.php | 15 ++ 38 files changed, 253 insertions(+), 330 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf214c83..a08971b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,8 @@ jobs: with: php-version: ${{ matrix.php }} coverage: xdebug + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: composer install - run: vendor/bin/phpunit --coverage-text if: ${{ matrix.php >= 7.3 }} @@ -37,6 +39,7 @@ jobs: name: PHPUnit (HHVM) runs-on: ubuntu-18.04 continue-on-error: true + if: false # temporarily skipped until https://github.com/azjezz/setup-hhvm/issues/3 is addressed steps: - uses: actions/checkout@v2 - uses: azjezz/setup-hhvm@v1 diff --git a/README.md b/README.md index 768b3f8c..126fdcaf 100644 --- a/README.md +++ b/README.md @@ -87,22 +87,17 @@ Once [installed](#install), you can use the following code to access a HTTP webserver and send some simple HTTP GET requests: ```php -$loop = React\EventLoop\Factory::create(); -$client = new React\Http\Browser($loop); +$client = new React\Http\Browser(); $client->get('http://www.google.com/')->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); }); - -$loop->run(); ``` This is an HTTP server which responds with `Hello World!` to every request. ```php -$loop = React\EventLoop\Factory::create(); - -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -112,10 +107,8 @@ $server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestI ); }); -$socket = new React\Socket\Server(8080, $loop); +$socket = new React\Socket\Server(8080); $server->listen($socket); - -$loop->run(); ``` See also the [examples](examples/). @@ -208,7 +201,7 @@ clean up any underlying resources. ```php $promise = $browser->get($url); -$loop->addTimer(2.0, function () use ($promise) { +Loop::addTimer(2.0, function () use ($promise) { $promise->cancel(); }); ``` @@ -266,9 +259,9 @@ like this: ```php $browser = new React\Http\Browser( - $loop, + null, new React\Socket\Connector( - $loop, + null, array( 'timeout' => 5 ) @@ -373,13 +366,12 @@ The resulting blocking code could look something like this: ```php use Clue\React\Block; -$loop = React\EventLoop\Factory::create(); -$browser = new React\Http\Browser($loop); +$browser = new React\Http\Browser(); $promise = $browser->get('http://example.com/'); try { - $response = Block\await($promise, $loop); + $response = Block\await($promise, Loop::get()); // response successfully received } catch (Exception $e) { // an error occured while performing the request @@ -394,7 +386,7 @@ $promises = array( $browser->get('http://www.example.org/'), ); -$responses = Block\awaitAll($promises, $loop); +$responses = Block\awaitAll($promises, Loop::get()); ``` Please refer to [clue/reactphp-block](https://github.com/clue/reactphp-block#readme) for more details. @@ -584,7 +576,7 @@ matching `Content-Length` request header like so: ```php $body = new React\Stream\ThroughStream(); -$loop->addTimer(1.0, function () use ($body) { +Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); @@ -610,15 +602,15 @@ protocol, such as plain HTTP and TLS-encrypted HTTPS. ```php $proxy = new Clue\React\HttpProxy\ProxyConnector( 'http://127.0.0.1:8080/', - new React\Socket\Connector($loop) + new React\Socket\Connector() ); -$connector = new React\Socket\Connector($loop, array( +$connector = new React\Socket\Connector(null, array( 'tcp' => $proxy, 'dns' => false )); -$browser = new React\Http\Browser($loop, $connector); +$browser = new React\Http\Browser(null, $connector); ``` See also the [HTTP CONNECT proxy example](examples/11-client-http-connect-proxy.php). @@ -637,15 +629,15 @@ only, this can technically be used to tunnel any TCP/IP-based protocol. ```php $proxy = new Clue\React\Socks\Client( 'socks://127.0.0.1:1080', - new React\Socket\Connector($loop) + new React\Socket\Connector() ); -$connector = new React\Socket\Connector($loop, array( +$connector = new React\Socket\Connector(null, array( 'tcp' => $proxy, 'dns' => false )); -$browser = new React\Http\Browser($loop, $connector); +$browser = new React\Http\Browser(null, $connector); ``` See also the [SOCKS proxy example](examples/12-client-socks-proxy.php). @@ -667,14 +659,14 @@ from the outside (database behind firewall) and as such can also be used for plain HTTP and TLS-encrypted HTTPS. ```php -$proxy = new Clue\React\SshProxy\SshSocksConnector('me@localhost:22', $loop); +$proxy = new Clue\React\SshProxy\SshSocksConnector('me@localhost:22', Loop::get()); -$connector = new React\Socket\Connector($loop, array( +$connector = new React\Socket\Connector(null, array( 'tcp' => $proxy, 'dns' => false )); -$browser = new React\Http\Browser($loop, $connector); +$browser = new React\Http\Browser(null, $connector); ``` See also the [SSH proxy example](examples/13-client-ssh-proxy.php). @@ -692,10 +684,10 @@ no longer be used to establish the connection: ```php $connector = new React\Socket\FixedUriConnector( 'unix:///var/run/docker.sock', - new React\Socket\UnixConnector($loop) + new React\Socket\UnixConnector() ); -$browser = new Browser($loop, $connector); +$browser = new Browser(null, $connector); $client->get('http://localhost/info')->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); @@ -718,7 +710,7 @@ the constructor and will be invoked with the respective [request](#server-reques object and expects a [response](#server-response) object in return: ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -737,6 +729,12 @@ Each outgoing HTTP response message is always represented by the [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), see also following [response](#server-response) chapter for more details. +This class takes an optional `LoopInterface|null $loop` parameter that can be used to +pass the event loop instance to use for this object. You can use a `null` value +here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). +This value SHOULD NOT be given unless you're sure you want to explicitly use a +given event loop instance. + In order to start listening for any incoming connections, the `Server` needs to be attached to an instance of [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) @@ -746,9 +744,9 @@ chapter. In its most simple form, you can attach this to a to start a plaintext HTTP server like this: ```php -$server = new React\Http\Server($loop, $handler); +$server = new React\Http\Server($handler); -$socket = new React\Socket\Server('0.0.0.0:8080', $loop); +$socket = new React\Socket\Server('0.0.0.0:8080'); $server->listen($socket); ``` @@ -817,7 +815,6 @@ once like this: ```php $server = new React\Http\Server( - $loop, new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -843,7 +840,6 @@ in memory: ```php $server = new React\Http\Server( - $loop, new React\Http\Middleware\StreamingRequestMiddleware(), $handler ); @@ -872,9 +868,9 @@ messages. In its most common form, you can attach this to a order to start a plaintext HTTP server like this: ```php -$server = new React\Http\Server($loop, $handler); +$server = new React\Http\Server($handler); -$socket = new React\Socket\Server('0.0.0.0:8080', $loop); +$socket = new React\Socket\Server('0.0.0.0:8080'); $server->listen($socket); ``` @@ -898,9 +894,9 @@ using a secure TLS listen address, a certificate file and optional `passphrase` like this: ```php -$server = new React\Http\Server($loop, $handler); +$server = new React\Http\Server($handler); -$socket = new React\Socket\Server('tls://0.0.0.0:8443', $loop, array( +$socket = new React\Socket\Server('tls://0.0.0.0:8443', null, array( 'local_cert' => __DIR__ . '/localhost.pem' )); $server->listen($socket); @@ -923,7 +919,7 @@ which in turn extends the and will be passed to the callback function like this. ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { $body = "The method of the request is: " . $request->getMethod(); $body .= "The requested path is: " . $request->getUri()->getPath(); @@ -966,7 +962,7 @@ The following parameters are currently available: Set to 'on' if the request used HTTPS, otherwise it won't be set ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; return new React\Http\Message\Response( @@ -991,7 +987,7 @@ The `getQueryParams(): array` method can be used to get the query parameters similiar to the `$_GET` variable. ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { $queryParams = $request->getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; @@ -1045,7 +1041,7 @@ By default, this method will only return parsed data for requests using request headers (commonly used for `POST` requests for HTML form submission data). ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { $name = $request->getParsedBody()['name'] ?? 'anonymous'; return new React\Http\Message\Response( @@ -1069,7 +1065,7 @@ an XML (`Content-Type: application/xml`) request body (which is commonly used fo `POST`, `PUT` or `PATCH` requests in JSON-based or RESTful/RESTish APIs). ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { $data = json_decode((string)$request->getBody()); $name = $data->name ?? 'anonymous'; @@ -1092,7 +1088,7 @@ This array will only be filled when using the `Content-Type: multipart/form-data request header (commonly used for `POST` requests for HTML file uploads). ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { $files = $request->getUploadedFiles(); $name = isset($files['avatar']) ? $files['avatar']->getClientFilename() : 'nothing'; @@ -1166,7 +1162,6 @@ gives you access to the incoming request body as the individual chunks arrive: ```php $server = new React\Http\Server( - $loop, new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $body = $request->getBody(); @@ -1240,7 +1235,6 @@ may be unknown (`null`) when using `Transfer-Encoding: chunked` for HTTP/1.1 req ```php $server = new React\Http\Server( - $loop, new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $size = $request->getBody()->getSize(); @@ -1313,7 +1307,7 @@ The `getCookieParams(): string[]` method can be used to get all cookies sent with the current request. ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { $key = 'react\php'; if (isset($request->getCookieParams()[$key])) { @@ -1385,7 +1379,7 @@ This projects ships a [`Response` class](#response) which implements the In its most simple form, you can use it like this: ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1413,9 +1407,9 @@ To prevent this you SHOULD use a This example shows how such a long-term action could look like: ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) use ($loop) { - return new Promise(function ($resolve, $reject) use ($loop) { - $loop->addTimer(1.5, function() use ($resolve) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { + return new Promise(function ($resolve, $reject) { + Loop::addTimer(1.5, function() use ($resolve) { $response = new React\Http\Message\Response( 200, array( @@ -1451,15 +1445,15 @@ Note that other implementations of the may only support strings. ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) use ($loop) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { $stream = new ThroughStream(); - $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { + $timer = Loop::addPeriodicTimer(0.5, function () use ($stream) { $stream->write(microtime(true) . PHP_EOL); }); - $loop->addTimer(5, function() use ($loop, $timer, $stream) { - $loop->cancelTimer($timer); + Loop::addTimer(5, function() use ($timer, $stream) { + Loop::cancelTimer($timer); $stream->end(); }); @@ -1543,7 +1537,7 @@ added automatically. This is the most common use case, for example when using a `string` response body like this: ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1562,10 +1556,10 @@ response messages will contain the plain response body. If you know the length of your streaming response body, you MAY want to specify it explicitly like this: ```php -$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) use ($loop) { +$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { $stream = new ThroughStream(); - $loop->addTimer(2.0, function () use ($stream) { + Loop::addTimer(2.0, function () use ($stream) { $stream->end("Hello World!\n"); }); @@ -1638,7 +1632,7 @@ A `Server: ReactPHP/1` response header will be added automatically. You can add a custom `Server` response header like this: ```php -$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server(function (ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1653,7 +1647,7 @@ don't want to expose the underlying server software), you can use an empty string value like this: ```php -$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server(function (ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1668,7 +1662,7 @@ date and time if none is given. You can add a custom `Date` response header like this: ```php -$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server(function (ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1683,7 +1677,7 @@ don't have an appropriate clock to rely on), you can use an empty string value like this: ```php -$server = new React\Http\Server($loop, function (ServerRequestInterface $request) { +$server = new React\Http\Server(function (ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1767,7 +1761,6 @@ header (`Request-Time`) and a final request handler that always returns a 200 co ```php $server = new React\Http\Server( - $loop, function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { $request = $request->withHeader('Request-Time', time()); return $next($request); @@ -1792,7 +1785,6 @@ In order to simplify handling both paths, you can simply wrap this in a ```php $server = new React\Http\Server( - $loop, function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { $promise = React\Promise\resolve($next($request)); return $promise->then(function (ResponseInterface $response) { @@ -1815,7 +1807,6 @@ handling logic (or logging etc.) by wrapping this in a ```php $server = new React\Http\Server( - $loop, function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { $promise = new React\Promise\Promise(function ($resolve) use ($next, $request) { $resolve($next($request)); @@ -1873,20 +1864,23 @@ feel free to add it to this list. The `React\Http\Browser` is responsible for sending HTTP requests to your HTTP server and keeps track of pending incoming HTTP responses. -It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage). ```php -$loop = React\EventLoop\Factory::create(); - -$browser = new React\Http\Browser($loop); +$browser = new React\Http\Browser(); ``` +This class takes an optional `LoopInterface|null $loop` parameter that can be used to +pass the event loop instance to use for this object. You can use a `null` value +here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). +This value SHOULD NOT be given unless you're sure you want to explicitly use a +given event loop instance. + 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( +$connector = new React\Socket\Connector(null, array( 'dns' => '127.0.0.1', 'tcp' => array( 'bindto' => '192.168.10.1:0' @@ -1897,7 +1891,7 @@ $connector = new React\Socket\Connector($loop, array( ) )); -$browser = new React\Http\Browser($loop, $connector); +$browser = new React\Http\Browser(null, $connector); ``` > Note that the browser class is final and shouldn't be extended, it is likely to be marked final in a future release. @@ -1959,7 +1953,7 @@ matching `Content-Length` request header like so: ```php $body = new React\Stream\ThroughStream(); -$loop->addTimer(1.0, function () use ($body) { +Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); @@ -2002,7 +1996,7 @@ matching `Content-Length` request header like so: ```php $body = new React\Stream\ThroughStream(); -$loop->addTimer(1.0, function () use ($body) { +Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); @@ -2036,7 +2030,7 @@ matching `Content-Length` request header like so: ```php $body = new React\Stream\ThroughStream(); -$loop->addTimer(1.0, function () use ($body) { +Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); @@ -2084,7 +2078,7 @@ explicitly pass in a matching `Content-Length` request header like so: ```php $body = new React\Stream\ThroughStream(); -$loop->addTimer(1.0, function () use ($body) { +Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); @@ -2146,7 +2140,7 @@ explicitly pass in a matching `Content-Length` request header like so: ```php $body = new React\Stream\ThroughStream(); -$loop->addTimer(1.0, function () use ($body) { +Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); @@ -2513,7 +2507,6 @@ than 10 handlers will be invoked at once: ```php $server = new React\Http\Server( - $loop, new React\Http\Middleware\LimitConcurrentRequestsMiddleware(10), $handler ); @@ -2525,7 +2518,6 @@ to limit the total number of requests that can be buffered at once: ```php $server = new React\Http\Server( - $loop, new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -2540,7 +2532,6 @@ processes one request after another without any concurrency: ```php $server = new React\Http\Server( - $loop, new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -2594,7 +2585,6 @@ Usage: ```php $server = new React\Http\Server( - $loop, new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB @@ -2656,7 +2646,6 @@ $handler = function (Psr\Http\Message\ServerRequestInterface $request) { }; $server = new React\Http\Server( - $loop, new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB diff --git a/composer.json b/composer.json index 6924ebff..9e30f4cf 100644 --- a/composer.json +++ b/composer.json @@ -29,11 +29,12 @@ "php": ">=5.3.0", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "psr/http-message": "^1.0", - "react/event-loop": "^1.0 || ^0.5", + "react/dns": "dev-default-loop#28e5df1 as 1.8.0", + "react/event-loop": "dev-master#78f7f43 as 1.2.0", "react/promise": "^2.3 || ^1.2.1", "react/promise-stream": "^1.1", - "react/socket": "^1.6", - "react/stream": "^1.1", + "react/socket": "dev-default-loop#b471dc7 as 1.8.0", + "react/stream": "dev-default-loop#e617d63 as 1.2.0", "ringcentral/psr7": "^1.2" }, "require-dev": { @@ -48,5 +49,19 @@ }, "autoload-dev": { "psr-4": { "React\\Tests\\Http\\": "tests" } - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/clue-labs/dns" + }, + { + "type": "vcs", + "url": "https://github.com/clue-labs/socket" + }, + { + "type": "vcs", + "url": "https://github.com/clue-labs/stream" + } + ] } diff --git a/examples/01-client-get-request.php b/examples/01-client-get-request.php index 31a82606..8e232398 100644 --- a/examples/01-client-get-request.php +++ b/examples/01-client-get-request.php @@ -5,11 +5,8 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = React\EventLoop\Factory::create(); -$client = new Browser($loop); +$client = new Browser(); $client->get('http://google.com/')->then(function (ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); }); - -$loop->run(); diff --git a/examples/02-client-concurrent-requests.php b/examples/02-client-concurrent-requests.php index 5a9e4258..dca1d9c1 100644 --- a/examples/02-client-concurrent-requests.php +++ b/examples/02-client-concurrent-requests.php @@ -5,8 +5,7 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = React\EventLoop\Factory::create(); -$client = new Browser($loop); +$client = new Browser(); $client->head('http://www.github.com/clue/http-react')->then(function (ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); @@ -19,5 +18,3 @@ $client->get('http://www.lueck.tv/psocksd')->then(function (ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); }); - -$loop->run(); diff --git a/examples/03-client-request-any.php b/examples/03-client-request-any.php index 881dabfc..a3bd2831 100644 --- a/examples/03-client-request-any.php +++ b/examples/03-client-request-any.php @@ -8,8 +8,7 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = React\EventLoop\Factory::create(); -$client = new Browser($loop); +$client = new Browser(); $promises = array( $client->head('http://www.github.com/clue/http-react'), @@ -28,5 +27,3 @@ var_dump($response->getHeaders()); echo PHP_EOL . $response->getBody(); }); - -$loop->run(); diff --git a/examples/04-client-post-json.php b/examples/04-client-post-json.php index 818dc9bc..400b1a13 100644 --- a/examples/04-client-post-json.php +++ b/examples/04-client-post-json.php @@ -5,8 +5,7 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = React\EventLoop\Factory::create(); -$client = new Browser($loop); +$client = new Browser(); $data = array( 'name' => array( @@ -25,5 +24,3 @@ )->then(function (ResponseInterface $response) { echo (string)$response->getBody(); }, 'printf'); - -$loop->run(); diff --git a/examples/05-client-put-xml.php b/examples/05-client-put-xml.php index 7c23182d..05804f23 100644 --- a/examples/05-client-put-xml.php +++ b/examples/05-client-put-xml.php @@ -5,8 +5,7 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = React\EventLoop\Factory::create(); -$client = new Browser($loop); +$client = new Browser(); $xml = new SimpleXMLElement(''); $child = $xml->addChild('user'); @@ -22,5 +21,3 @@ )->then(function (ResponseInterface $response) { echo (string)$response->getBody(); }, 'printf'); - -$loop->run(); diff --git a/examples/11-client-http-connect-proxy.php b/examples/11-client-http-connect-proxy.php index 53d2e91a..afa22e83 100644 --- a/examples/11-client-http-connect-proxy.php +++ b/examples/11-client-http-connect-proxy.php @@ -6,29 +6,24 @@ // $ php examples/72-server-http-connect-proxy.php 8080 // $ php examples/11-client-http-connect-proxy.php -use React\Http\Browser; use Clue\React\HttpProxy\ProxyConnector as HttpConnectClient; use Psr\Http\Message\ResponseInterface; -use React\EventLoop\Factory as LoopFactory; +use React\Http\Browser; use React\Socket\Connector; require __DIR__ . '/../vendor/autoload.php'; -$loop = LoopFactory::create(); - // create a new HTTP CONNECT proxy client which connects to a HTTP CONNECT proxy server listening on localhost:8080 -$proxy = new HttpConnectClient('127.0.0.1:8080', new Connector($loop)); +$proxy = new HttpConnectClient('127.0.0.1:8080', new Connector()); // create a Browser object that uses the HTTP CONNECT proxy client for connections -$connector = new Connector($loop, array( +$connector = new Connector(null, array( 'tcp' => $proxy, 'dns' => false )); -$browser = new Browser($loop, $connector); +$browser = new Browser(null, $connector); // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { echo RingCentral\Psr7\str($response); }, 'printf'); - -$loop->run(); diff --git a/examples/12-client-socks-proxy.php b/examples/12-client-socks-proxy.php index 49827fa2..ec2375c5 100644 --- a/examples/12-client-socks-proxy.php +++ b/examples/12-client-socks-proxy.php @@ -3,29 +3,24 @@ // not already running a SOCKS proxy server? // Try LeProxy.org or this: `ssh -D 1080 localhost` -use React\Http\Browser; use Clue\React\Socks\Client as SocksClient; use Psr\Http\Message\ResponseInterface; -use React\EventLoop\Factory as LoopFactory; +use React\Http\Browser; use React\Socket\Connector; require __DIR__ . '/../vendor/autoload.php'; -$loop = LoopFactory::create(); - // create a new SOCKS proxy client which connects to a SOCKS proxy server listening on localhost:1080 -$proxy = new SocksClient('127.0.0.1:1080', new Connector($loop)); +$proxy = new SocksClient('127.0.0.1:1080', new Connector()); // create a Browser object that uses the SOCKS proxy client for connections -$connector = new Connector($loop, array( +$connector = new Connector(null, array( 'tcp' => $proxy, 'dns' => false )); -$browser = new Browser($loop, $connector); +$browser = new Browser(null, $connector); // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { echo RingCentral\Psr7\str($response); }, 'printf'); - -$loop->run(); diff --git a/examples/13-client-ssh-proxy.php b/examples/13-client-ssh-proxy.php index d0424fea..d4c8dcea 100644 --- a/examples/13-client-ssh-proxy.php +++ b/examples/13-client-ssh-proxy.php @@ -1,29 +1,25 @@ $proxy, 'dns' => false )); -$browser = new Browser($loop, $connector); +$browser = new Browser(null, $connector); // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { echo RingCentral\Psr7\str($response); }, 'printf'); - -$loop->run(); diff --git a/examples/14-client-unix-domain-sockets.php b/examples/14-client-unix-domain-sockets.php index 8881321e..f60dd9f7 100644 --- a/examples/14-client-unix-domain-sockets.php +++ b/examples/14-client-unix-domain-sockets.php @@ -1,27 +1,22 @@ get('http://localhost/info')->then(function (ResponseInterface $response) { echo Psr7\str($response); }, 'printf'); - -$loop->run(); diff --git a/examples/21-client-request-streaming-to-stdout.php b/examples/21-client-request-streaming-to-stdout.php index b7873775..3d2110a2 100644 --- a/examples/21-client-request-streaming-to-stdout.php +++ b/examples/21-client-request-streaming-to-stdout.php @@ -13,11 +13,10 @@ exit(1); } -$loop = React\EventLoop\Factory::create(); -$client = new Browser($loop); +$client = new Browser(); -$out = new WritableResourceStream(STDOUT, $loop); -$info = new WritableResourceStream(STDERR, $loop); +$out = new WritableResourceStream(STDOUT); +$info = new WritableResourceStream(STDERR); $url = isset($argv[1]) ? $argv[1] : 'http://google.com/'; $info->write('Requesting ' . $url . '…' . PHP_EOL); @@ -29,5 +28,3 @@ assert($body instanceof ReadableStreamInterface); $body->pipe($out); }, 'printf'); - -$loop->run(); diff --git a/examples/22-client-stream-upload-from-stdin.php b/examples/22-client-stream-upload-from-stdin.php index 4a36df91..a0857feb 100644 --- a/examples/22-client-stream-upload-from-stdin.php +++ b/examples/22-client-stream-upload-from-stdin.php @@ -1,7 +1,7 @@ post($url, array(), $in)->then(function (ResponseInterface $response) { echo 'Received' . PHP_EOL . Psr7\str($response); }, 'printf'); - -$loop->run(); diff --git a/examples/51-server-hello-world.php b/examples/51-server-hello-world.php index f6903cff..2cfd5649 100644 --- a/examples/51-server-hello-world.php +++ b/examples/51-server-hello-world.php @@ -1,15 +1,12 @@ listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/52-server-count-visitors.php b/examples/52-server-count-visitors.php index 2b8e897c..ae173bbd 100644 --- a/examples/52-server-count-visitors.php +++ b/examples/52-server-count-visitors.php @@ -1,16 +1,13 @@ listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/53-server-whatsmyip.php b/examples/53-server-whatsmyip.php index 18f7504e..82f34742 100644 --- a/examples/53-server-whatsmyip.php +++ b/examples/53-server-whatsmyip.php @@ -1,15 +1,12 @@ getServerParams()['REMOTE_ADDR']; return new Response( @@ -21,9 +18,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/54-server-query-parameter.php b/examples/54-server-query-parameter.php index 2786f380..dbddfbc0 100644 --- a/examples/54-server-query-parameter.php +++ b/examples/54-server-query-parameter.php @@ -1,15 +1,12 @@ getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; @@ -28,9 +25,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index 6faf6be7..55370c3b 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -1,15 +1,12 @@ getCookieParams()[$key])) { @@ -34,9 +31,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/56-server-sleep.php b/examples/56-server-sleep.php index 3da6963b..9f149c4c 100644 --- a/examples/56-server-sleep.php +++ b/examples/56-server-sleep.php @@ -1,18 +1,16 @@ addTimer(1.5, function() use ($resolve) { +$server = new Server(function (ServerRequestInterface $request) { + return new Promise(function ($resolve, $reject) { + Loop::addTimer(1.5, function() use ($resolve) { $response = new Response( 200, array( @@ -25,9 +23,7 @@ }); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/57-server-error-handling.php b/examples/57-server-error-handling.php index c8e99ee4..6952a559 100644 --- a/examples/57-server-error-handling.php +++ b/examples/57-server-error-handling.php @@ -1,17 +1,14 @@ listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php index 518c2cb4..941ded1f 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -1,16 +1,14 @@ getMethod() !== 'GET' || $request->getUri()->getPath() !== '/') { return new Response(404); } @@ -18,18 +16,18 @@ $stream = new ThroughStream(); // send some data every once in a while with periodic timer - $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) { + $timer = Loop::addPeriodicTimer(0.5, function () use ($stream) { $stream->write(microtime(true) . PHP_EOL); }); // demo for ending stream after a few seconds - $loop->addTimer(5.0, function() use ($stream) { + Loop::addTimer(5.0, function() use ($stream) { $stream->end(); }); // stop timer if stream is closed (such as when connection is closed) - $stream->on('close', function () use ($loop, $timer) { - $loop->cancelTimer($timer); + $stream->on('close', function () use ($timer) { + Loop::cancelTimer($timer); }); return new Response( @@ -41,9 +39,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/59-server-json-api.php b/examples/59-server-json-api.php index 8602a889..c1b4d305 100644 --- a/examples/59-server-json-api.php +++ b/examples/59-server-json-api.php @@ -7,15 +7,12 @@ // $ curl -v http://localhost:8080/ -H 'Content-Type: application/json' -d '{"name":"Alice"}' use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; use React\Http\Message\Response; use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; -$loop = Factory::create(); - -$server = new Server($loop, function (ServerRequestInterface $request) { +$server = new Server(function (ServerRequestInterface $request) { if ($request->getHeaderLine('Content-Type') !== 'application/json') { return new Response( 415, // Unsupported Media Type @@ -56,9 +53,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index dfe3e941..5b671618 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -1,15 +1,12 @@ isset($argv[2]) ? $argv[2] : __DIR__ . '/localhost.pem' )); $server->listen($socket); @@ -28,5 +25,3 @@ //$socket->on('error', 'printf'); echo 'Listening on ' . str_replace('tls:', 'https:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index d7eef4f3..b1f0d8ee 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -9,7 +9,6 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\UploadedFileInterface; -use React\EventLoop\Factory; use React\Http\Message\Response; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; @@ -19,8 +18,6 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = Factory::create(); - $handler = function (ServerRequestInterface $request) { if ($request->getMethod() === 'POST') { // Take form input values from POST values (for illustration purposes only!) @@ -125,7 +122,6 @@ // Note how this example explicitly uses the advanced `StreamingRequestMiddleware` to apply // custom request buffering limits below before running our request handler. $server = new Server( - $loop, new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers, queue otherwise new RequestBodyBufferMiddleware(8 * 1024 * 1024), // 8 MiB max, ignore body otherwise @@ -133,9 +129,7 @@ $handler ); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', null); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index 45eb0dea..c2416e15 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -1,16 +1,11 @@ getBody(); @@ -49,9 +44,7 @@ function (Psr\Http\Message\ServerRequestInterface $request) { $server->on('error', 'printf'); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index b959b7bf..95c2f411 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -11,13 +11,11 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = Factory::create(); - // Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // This means that this proxy buffers the whole request before "processing" it. // As such, this is store-and-forward proxy. This could also use the advanced // `StreamingRequestMiddleware` to forward the incoming request as it comes in. -$server = new Server($loop, function (RequestInterface $request) { +$server = new Server(function (RequestInterface $request) { if (strpos($request->getRequestTarget(), '://') === false) { return new Response( 400, @@ -48,9 +46,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/72-server-http-connect-proxy.php b/examples/72-server-http-connect-proxy.php index e786da76..a1f33983 100644 --- a/examples/72-server-http-connect-proxy.php +++ b/examples/72-server-http-connect-proxy.php @@ -4,7 +4,6 @@ // $ curl -v --proxy http://localhost:8080 https://reactphp.org/ use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; use React\Http\Message\Response; use React\Http\Server; use React\Socket\Connector; @@ -12,14 +11,13 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = Factory::create(); -$connector = new Connector($loop); +$connector = new Connector(); // Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // Unlike the plain HTTP proxy, the CONNECT method does not contain a body // and we establish an end-to-end connection over the stream object, so this // doesn't have to store any payload data in memory at all. -$server = new Server($loop, function (ServerRequestInterface $request) use ($connector) { +$server = new Server(function (ServerRequestInterface $request) use ($connector) { if ($request->getMethod() !== 'CONNECT') { return new Response( 405, @@ -53,9 +51,7 @@ function ($e) { ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/81-server-upgrade-echo.php b/examples/81-server-upgrade-echo.php index 34e85f6c..6a20181a 100644 --- a/examples/81-server-upgrade-echo.php +++ b/examples/81-server-upgrade-echo.php @@ -18,19 +18,17 @@ */ use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; +use React\EventLoop\Loop; use React\Http\Message\Response; use React\Http\Server; use React\Stream\ThroughStream; require __DIR__ . '/../vendor/autoload.php'; -$loop = Factory::create(); - // Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // The initial incoming request does not contain a body and we upgrade to a // stream object below. -$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { +$server = new Server(function (ServerRequestInterface $request) { if ($request->getHeaderLine('Upgrade') !== 'echo' || $request->getProtocolVersion() === '1.0') { return new Response( 426, @@ -46,7 +44,7 @@ // this means that any Upgraded data will simply be sent back to the client $stream = new ThroughStream(); - $loop->addTimer(0, function () use ($stream) { + Loop::addTimer(0, function () use ($stream) { $stream->write("Hello! Anything you send will be piped back." . PHP_EOL); }); @@ -59,9 +57,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/82-server-upgrade-chat.php b/examples/82-server-upgrade-chat.php index 5e49ce37..2c6f08a8 100644 --- a/examples/82-server-upgrade-chat.php +++ b/examples/82-server-upgrade-chat.php @@ -20,7 +20,7 @@ */ use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; +use React\EventLoop\Loop; use React\Http\Message\Response; use React\Http\Server; use React\Stream\CompositeStream; @@ -28,8 +28,6 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = Factory::create(); - // simply use a shared duplex ThroughStream for all clients // it will simply emit any data that is sent to it // this means that any Upgraded data will simply be sent back to the client @@ -38,7 +36,7 @@ // Note how this example uses the `Server` without the `StreamingRequestMiddleware`. // The initial incoming request does not contain a body and we upgrade to a // stream object below. -$server = new Server($loop, function (ServerRequestInterface $request) use ($loop, $chat) { +$server = new Server(function (ServerRequestInterface $request) use ($chat) { if ($request->getHeaderLine('Upgrade') !== 'chat' || $request->getProtocolVersion() === '1.0') { return new Response( 426, @@ -68,7 +66,7 @@ }); // say hello to new user - $loop->addTimer(0, function () use ($chat, $username, $out) { + Loop::addTimer(0, function () use ($chat, $username, $out) { $out->write('Welcome to this chat example, ' . $username . '!' . PHP_EOL); $chat->write($username . ' joined' . PHP_EOL); }); @@ -87,9 +85,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/examples/91-client-benchmark-download.php b/examples/91-client-benchmark-download.php index 4fd39b9d..2f76a3f3 100644 --- a/examples/91-client-benchmark-download.php +++ b/examples/91-client-benchmark-download.php @@ -11,8 +11,9 @@ // b2) run HTTP client receiving a 10 GB download: // $ php examples/91-client-benchmark-download.php http://localhost:8080/10g.bin -use React\Http\Browser; use Psr\Http\Message\ResponseInterface; +use React\EventLoop\Loop; +use React\Http\Browser; use React\Stream\ReadableStreamInterface; $url = isset($argv[1]) ? $argv[1] : 'http://google.com/'; @@ -23,12 +24,11 @@ echo 'NOTICE: The "xdebug" extension is loaded, this has a major impact on performance.' . PHP_EOL; } -$loop = React\EventLoop\Factory::create(); -$client = new Browser($loop); +$client = new Browser(); echo 'Requesting ' . $url . '…' . PHP_EOL; -$client->requestStreaming('GET', $url)->then(function (ResponseInterface $response) use ($loop) { +$client->requestStreaming('GET', $url)->then(function (ResponseInterface $response) { echo 'Headers received' . PHP_EOL; echo RingCentral\Psr7\str($response); @@ -42,19 +42,17 @@ }); // report progress every 0.1s - $timer = $loop->addPeriodicTimer(0.1, function () use (&$bytes) { + $timer = Loop::addPeriodicTimer(0.1, function () use (&$bytes) { echo "\rDownloaded " . $bytes . " bytes…"; }); // report results once the stream closes $time = microtime(true); - $stream->on('close', function() use (&$bytes, $timer, $loop, $time) { - $loop->cancelTimer($timer); + $stream->on('close', function() use (&$bytes, $timer, $time) { + Loop::cancelTimer($timer); $time = microtime(true) - $time; echo "\r" . 'Downloaded ' . $bytes . ' bytes in ' . round($time, 3) . 's => ' . round($bytes / $time / 1000000, 1) . ' MB/s' . PHP_EOL; }); }, 'printf'); - -$loop->run(); diff --git a/examples/92-client-benchmark-upload.php b/examples/92-client-benchmark-upload.php index cc2cf0c0..bd767966 100644 --- a/examples/92-client-benchmark-upload.php +++ b/examples/92-client-benchmark-upload.php @@ -11,9 +11,10 @@ // b2) run HTTP client sending a 10 GB upload // $ php examples/92-client-benchmark-upload.php http://localhost:8080/ 10000 -use React\Http\Browser; use Evenement\EventEmitter; use Psr\Http\Message\ResponseInterface; +use React\EventLoop\Loop; +use React\Http\Browser; use React\Stream\ReadableStreamInterface; use React\Stream\Util; use React\Stream\WritableStreamInterface; @@ -92,33 +93,30 @@ public function getPosition() } } -$loop = React\EventLoop\Factory::create(); -$client = new Browser($loop); +$client = new Browser(); $url = isset($argv[1]) ? $argv[1] : 'http://httpbin.org/post'; $n = isset($argv[2]) ? $argv[2] : 10; $source = new ChunkRepeater(str_repeat('x', 1000000), $n); -$loop->futureTick(function () use ($source) { +Loop::futureTick(function () use ($source) { $source->resume(); }); echo 'POSTing ' . $n . ' MB to ' . $url . PHP_EOL; $start = microtime(true); -$report = $loop->addPeriodicTimer(0.05, function () use ($source, $start) { +$report = Loop::addPeriodicTimer(0.05, function () use ($source, $start) { printf("\r%d bytes in %0.3fs...", $source->getPosition(), microtime(true) - $start); }); -$client->post($url, array('Content-Length' => $n * 1000000), $source)->then(function (ResponseInterface $response) use ($source, $report, $loop, $start) { +$client->post($url, array('Content-Length' => $n * 1000000), $source)->then(function (ResponseInterface $response) use ($source, $report, $start) { $now = microtime(true); - $loop->cancelTimer($report); + Loop::cancelTimer($report); printf("\r%d bytes in %0.3fs => %.1f MB/s\n", $source->getPosition(), $now - $start, $source->getPosition() / ($now - $start) / 1000000); echo rtrim(preg_replace('/x{5,}/','x…', (string) $response->getBody()), PHP_EOL) . PHP_EOL; -}, function ($e) use ($loop, $report) { - $loop->cancelTimer($report); +}, function ($e) use ($report) { + Loop::cancelTimer($report); echo 'Error: ' . $e->getMessage() . PHP_EOL; }); - -$loop->run(); diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index 1a49df72..a6b4e9c1 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -16,7 +16,6 @@ use Evenement\EventEmitter; use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; use React\Http\Message\Response; use React\Http\Server; use React\Stream\ReadableStreamInterface; @@ -24,8 +23,6 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = Factory::create(); - /** A readable stream that can emit a lot of data */ class ChunkRepeater extends EventEmitter implements ReadableStreamInterface { @@ -94,7 +91,7 @@ public function getSize() } } -$server = new Server($loop, function (ServerRequestInterface $request) use ($loop) { +$server = new Server(function (ServerRequestInterface $request) { switch ($request->getUri()->getPath()) { case '/': return new Response( @@ -114,7 +111,7 @@ public function getSize() return new Response(404); } - $loop->addTimer(0, array($stream, 'resume')); + React\EventLoop\Loop::addTimer(0, array($stream, 'resume')); return new Response( 200, @@ -126,9 +123,7 @@ public function getSize() ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); +$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $server->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; - -$loop->run(); diff --git a/src/Browser.php b/src/Browser.php index 188320e8..ed83689b 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -5,6 +5,7 @@ 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; @@ -26,20 +27,23 @@ class Browser /** * The `Browser` is responsible for sending HTTP requests to your HTTP server * and keeps track of pending incoming HTTP responses. - * It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage). * * ```php - * $loop = React\EventLoop\Factory::create(); - * - * $browser = new React\Http\Browser($loop); + * $browser = new React\Http\Browser(); * ``` * + * This class takes an optional `LoopInterface|null $loop` parameter that can be used to + * pass the event loop instance to use for this object. You can use a `null` value + * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). + * This value SHOULD NOT be given unless you're sure you want to explicitly use a + * given event loop instance. + * * 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( + * $connector = new React\Socket\Connector(null, array( * 'dns' => '127.0.0.1', * 'tcp' => array( * 'bindto' => '192.168.10.1:0' @@ -50,15 +54,16 @@ class Browser * ) * )); * - * $browser = new React\Http\Browser($loop, $connector); + * $browser = new React\Http\Browser(null, $connector); * ``` * - * @param LoopInterface $loop - * @param ConnectorInterface|null $connector [optional] Connector to use. + * @param ?LoopInterface $loop + * @param ?ConnectorInterface $connector [optional] Connector to use. * Should be `null` in order to use default Connector. */ - public function __construct(LoopInterface $loop, ConnectorInterface $connector = null) + public function __construct(LoopInterface $loop = null, ConnectorInterface $connector = null) { + $loop = $loop ?: Loop::get(); $this->transaction = new Transaction( Sender::createFromLoop($loop, $connector), $loop @@ -127,7 +132,7 @@ public function get($url, array $headers = array()) * * ```php * $body = new React\Stream\ThroughStream(); - * $loop->addTimer(1.0, function () use ($body) { + * Loop::addTimer(1.0, function () use ($body) { * $body->end("hello world"); * }); * @@ -185,7 +190,7 @@ public function head($url, array $headers = array()) * * ```php * $body = new React\Stream\ThroughStream(); - * $loop->addTimer(1.0, function () use ($body) { + * Loop::addTimer(1.0, function () use ($body) { * $body->end("hello world"); * }); * @@ -227,7 +232,7 @@ public function patch($url, array $headers = array(), $contents = '') * * ```php * $body = new React\Stream\ThroughStream(); - * $loop->addTimer(1.0, function () use ($body) { + * Loop::addTimer(1.0, function () use ($body) { * $body->end("hello world"); * }); * @@ -291,7 +296,7 @@ public function delete($url, array $headers = array(), $contents = '') * * ```php * $body = new React\Stream\ThroughStream(); - * $loop->addTimer(1.0, function () use ($body) { + * Loop::addTimer(1.0, function () use ($body) { * $body->end("hello world"); * }); * @@ -362,7 +367,7 @@ public function request($method, $url, array $headers = array(), $body = '') * * ```php * $body = new React\Stream\ThroughStream(); - * $loop->addTimer(1.0, function () use ($body) { + * Loop::addTimer(1.0, function () use ($body) { * $body->end("hello world"); * }); * diff --git a/src/Middleware/LimitConcurrentRequestsMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php index 9aaf5ff2..cc1dfc42 100644 --- a/src/Middleware/LimitConcurrentRequestsMiddleware.php +++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -30,7 +30,6 @@ * * ```php * $server = new React\Http\Server( - * $loop, * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(10), * $handler @@ -43,7 +42,6 @@ * * ```php * $server = new React\Http\Server( - * $loop, * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -58,7 +56,6 @@ * * ```php * $server = new React\Http\Server( - * $loop, * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request diff --git a/src/Server.php b/src/Server.php index 1aa5d405..c2e3f5a6 100644 --- a/src/Server.php +++ b/src/Server.php @@ -3,6 +3,7 @@ namespace React\Http; use Evenement\EventEmitter; +use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use React\Http\Io\IniUtil; use React\Http\Io\MiddlewareRunner; @@ -23,7 +24,7 @@ * object and expects a [response](#server-response) object in return: * * ```php - * $server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) { + * $server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { * return new React\Http\Message\Response( * 200, * array( @@ -42,6 +43,12 @@ * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), * see also following [response](#server-response) chapter for more details. * + * This class takes an optional `LoopInterface|null $loop` parameter that can be used to + * pass the event loop instance to use for this object. You can use a `null` value + * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). + * This value SHOULD NOT be given unless you're sure you want to explicitly use a + * given event loop instance. + * * In order to start listening for any incoming connections, the `Server` needs * to be attached to an instance of * [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) @@ -51,9 +58,9 @@ * to start a plaintext HTTP server like this: * * ```php - * $server = new React\Http\Server($loop, $handler); + * $server = new React\Http\Server($handler); * - * $socket = new React\Socket\Server('0.0.0.0:8080', $loop); + * $socket = new React\Socket\Server('0.0.0.0:8080'); * $server->listen($socket); * ``` * @@ -122,7 +129,6 @@ * * ```php * $server = new React\Http\Server( - * $loop, * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -148,7 +154,6 @@ * * ```php * $server = new React\Http\Server( - * $loop, * new React\Http\Middleware\StreamingRequestMiddleware(), * $handler * ); @@ -191,14 +196,19 @@ final class Server extends EventEmitter * connections in order to then parse incoming data as HTTP. * See also [listen()](#listen) for more details. * - * @param LoopInterface $loop + * @param callable|LoopInterface $requestHandlerOrLoop * @param callable[] ...$requestHandler * @see self::listen() */ - public function __construct(LoopInterface $loop) + public function __construct($requestHandlerOrLoop) { $requestHandlers = \func_get_args(); - \array_shift($requestHandlers); + if (reset($requestHandlers) instanceof LoopInterface) { + $loop = \array_shift($requestHandlers); + } else { + $loop = Loop::get(); + } + $requestHandlersCount = \count($requestHandlers); if ($requestHandlersCount === 0 || \count(\array_filter($requestHandlers, 'is_callable')) < $requestHandlersCount) { throw new \InvalidArgumentException('Invalid request handler given'); @@ -253,9 +263,9 @@ public function __construct(LoopInterface $loop) * order to start a plaintext HTTP server like this: * * ```php - * $server = new React\Http\Server($loop, $handler); + * $server = new React\Http\Server($handler); * - * $socket = new React\Socket\Server(8080, $loop); + * $socket = new React\Socket\Server(8080); * $server->listen($socket); * ``` * @@ -279,9 +289,9 @@ public function __construct(LoopInterface $loop) * `passphrase` like this: * * ```php - * $server = new React\Http\Server($loop, $handler); + * $server = new React\Http\Server($handler); * - * $socket = new React\Socket\Server('tls://0.0.0.0:8443', $loop, array( + * $socket = new React\Socket\Server('tls://0.0.0.0:8443', null, array( * 'local_cert' => __DIR__ . '/localhost.pem' * )); * $server->listen($socket); diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index 612875fc..5df2b837 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -28,6 +28,21 @@ public function setUpBrowser() $ref->setValue($this->browser, $this->sender); } + public function testConstructWithoutLoopAssignsLoopAutomatically() + { + $browser = new Browser(); + + $ref = new \ReflectionProperty($browser, 'transaction'); + $ref->setAccessible(true); + $transaction = $ref->getValue($browser); + + $ref = new \ReflectionProperty($transaction, 'loop'); + $ref->setAccessible(true); + $loop = $ref->getValue($transaction); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + } + public function testGetSendsGetRequest() { $that = $this; diff --git a/tests/ServerTest.php b/tests/ServerTest.php index ff2cd9c1..6cb1b9b5 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -46,6 +46,21 @@ public function setUpConnectionMockAndSocket() $this->socket = new SocketServerStub(); } + public function testConstructWithoutLoopAssignsLoopAutomatically() + { + $server = new Server(function () { }); + + $ref = new \ReflectionProperty($server, 'streamingServer'); + $ref->setAccessible(true); + $streamingServer = $ref->getValue($server); + + $ref = new \ReflectionProperty($streamingServer, 'loop'); + $ref->setAccessible(true); + $loop = $ref->getValue($streamingServer); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + } + public function testInvalidCallbackFunctionLeadsToException() { $this->setExpectedException('InvalidArgumentException'); From 6b1a8261b2ac18ab219c9c949c789bb827b099e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 8 Jul 2021 16:46:55 +0200 Subject: [PATCH 061/152] Update to stable reactphp/event-loop v1.2.0 & updated stream and socket --- .github/workflows/ci.yml | 3 --- composer.json | 23 ++++------------------- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a08971b8..cf214c83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,8 +27,6 @@ jobs: with: php-version: ${{ matrix.php }} coverage: xdebug - env: - COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: composer install - run: vendor/bin/phpunit --coverage-text if: ${{ matrix.php >= 7.3 }} @@ -39,7 +37,6 @@ jobs: name: PHPUnit (HHVM) runs-on: ubuntu-18.04 continue-on-error: true - if: false # temporarily skipped until https://github.com/azjezz/setup-hhvm/issues/3 is addressed steps: - uses: actions/checkout@v2 - uses: azjezz/setup-hhvm@v1 diff --git a/composer.json b/composer.json index 9e30f4cf..6673b232 100644 --- a/composer.json +++ b/composer.json @@ -29,12 +29,11 @@ "php": ">=5.3.0", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "psr/http-message": "^1.0", - "react/dns": "dev-default-loop#28e5df1 as 1.8.0", - "react/event-loop": "dev-master#78f7f43 as 1.2.0", + "react/event-loop": "^1.2", "react/promise": "^2.3 || ^1.2.1", "react/promise-stream": "^1.1", - "react/socket": "dev-default-loop#b471dc7 as 1.8.0", - "react/stream": "dev-default-loop#e617d63 as 1.2.0", + "react/socket": "^1.8", + "react/stream": "^1.2", "ringcentral/psr7": "^1.2" }, "require-dev": { @@ -49,19 +48,5 @@ }, "autoload-dev": { "psr-4": { "React\\Tests\\Http\\": "tests" } - }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/clue-labs/dns" - }, - { - "type": "vcs", - "url": "https://github.com/clue-labs/socket" - }, - { - "type": "vcs", - "url": "https://github.com/clue-labs/stream" - } - ] + } } From d9401a5c1be1ed967ee4d94187371cedfe1e8d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 11 Jul 2021 14:57:54 +0200 Subject: [PATCH 062/152] Work around failing tests on legacy PHP 5.3 --- tests/Client/FunctionalIntegrationTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 2db75b35..3e07803a 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -83,6 +83,9 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp /** @group internet */ public function testSuccessfulResponseEmitsEnd() { + // 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); + $loop = Factory::create(); $client = new Client($loop); @@ -106,6 +109,9 @@ public function testPostDataReturnsData() $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); + $loop = Factory::create(); $client = new Client($loop); From b008fb855ac446e46fab9f3eebb23a797f311595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 11 Jul 2021 15:03:19 +0200 Subject: [PATCH 063/152] Prepare v1.4.0 release --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ae6bc15..f562f66d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 1.4.0 (2021-07-11) + +A major new feature release, see [**release announcement**](https://clue.engineering/2021/announcing-reactphp-default-loop). + +* Feature: Simplify usage by supporting new [default loop](https://reactphp.org/event-loop/#loop). + (#410 by @clue) + + ```php + // old (still supported) + $browser = new React\Http\Browser($loop); + $server = new React\Http\Server($loop, $handler); + + // new (using default loop) + $browser = new React\Http\Browser(); + $server = new React\Http\Server($handler); + ``` + ## 1.3.0 (2021-04-11) * Feature: Support persistent connections (`Connection: keep-alive`). From e654f59a267e72a157916a8adeb48c6f961bfea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 24 Jul 2021 14:46:19 +0200 Subject: [PATCH 064/152] Rename `Server` to `HttpServer` to avoid class name collisions --- README.md | 128 +++++++++--------- examples/51-server-hello-world.php | 7 +- examples/52-server-count-visitors.php | 7 +- examples/53-server-whatsmyip.php | 7 +- examples/54-server-query-parameter.php | 7 +- examples/55-server-cookie-handling.php | 7 +- examples/56-server-sleep.php | 7 +- examples/57-server-error-handling.php | 7 +- examples/58-server-stream-response.php | 7 +- examples/59-server-json-api.php | 7 +- examples/61-server-hello-world-https.php | 9 +- examples/62-server-form-upload.php | 7 +- examples/63-server-streaming-request.php | 8 +- examples/71-server-http-proxy.php | 10 +- examples/72-server-http-connect-proxy.php | 9 +- examples/81-server-upgrade-echo.php | 9 +- examples/82-server-upgrade-chat.php | 9 +- examples/99-server-benchmark-download.php | 7 +- src/{Server.php => HttpServer.php} | 32 +++-- src/Io/StreamingServer.php | 8 +- .../LimitConcurrentRequestsMiddleware.php | 6 +- src/Middleware/StreamingRequestMiddleware.php | 10 +- tests/FunctionalBrowserTest.php | 18 +-- ...rTest.php => FunctionalHttpServerTest.php} | 106 +++++++-------- tests/{ServerTest.php => HttpServerTest.php} | 98 +++++++------- 25 files changed, 264 insertions(+), 273 deletions(-) rename src/{Server.php => HttpServer.php} (93%) rename tests/{FunctionalServerTest.php => FunctionalHttpServerTest.php} (90%) rename tests/{ServerTest.php => HttpServerTest.php} (81%) diff --git a/README.md b/README.md index 126fdcaf..d0fe6961 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ multiple concurrent HTTP requests without blocking. * [SSH proxy](#ssh-proxy) * [Unix domain sockets](#unix-domain-sockets) * [Server Usage](#server-usage) - * [Server](#server) + * [HttpServer](#httpserver) * [listen()](#listen) * [Server Request](#server-request) * [Request parameters](#request-parameters) @@ -97,7 +97,7 @@ $client->get('http://www.google.com/')->then(function (Psr\Http\Message\Response This is an HTTP server which responds with `Hello World!` to every request. ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -108,7 +108,7 @@ $server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterfac }); $socket = new React\Socket\Server(8080); -$server->listen($socket); +$http->listen($socket); ``` See also the [examples](examples/). @@ -699,9 +699,11 @@ See also the [Unix Domain Sockets (UDS) example](examples/14-client-unix-domain- ## Server Usage -### Server +### HttpServer -The `React\Http\Server` class is responsible for handling incoming connections and then + + +The `React\Http\HttpServer` class is responsible for handling incoming connections and then processing each incoming HTTP request. When a complete HTTP request has been received, it will invoke the given @@ -710,7 +712,7 @@ the constructor and will be invoked with the respective [request](#server-reques object and expects a [response](#server-response) object in return: ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -735,7 +737,7 @@ here in order to use the [default loop](https://github.com/reactphp/event-loop#l This value SHOULD NOT be given unless you're sure you want to explicitly use a given event loop instance. -In order to start listening for any incoming connections, the `Server` needs +In order to start listening for any incoming connections, the `HttpServer` needs to be attached to an instance of [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) through the [`listen()`](#listen) method as described in the following @@ -744,17 +746,17 @@ chapter. In its most simple form, you can attach this to a to start a plaintext HTTP server like this: ```php -$server = new React\Http\Server($handler); +$http = new React\Http\HttpServer($handler); $socket = new React\Socket\Server('0.0.0.0:8080'); -$server->listen($socket); +$http->listen($socket); ``` See also the [`listen()`](#listen) method and the [hello world server example](examples/51-server-hello-world.php) for more details. -By default, the `Server` buffers and parses the complete incoming HTTP +By default, the `HttpServer` buffers and parses the complete incoming HTTP request in memory. It will invoke the given request handler function when the complete request headers and request body has been received. This means the [request](#server-request) object passed to your request handler function will be @@ -807,14 +809,14 @@ limit to allow for more concurrent requests (set `memory_limit 512M` or more) or explicitly limit concurrency. In order to override the above buffering defaults, you can configure the -`Server` explicitly. You can use the +`HttpServer` explicitly. You can use the [`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) to explicitly configure the total number of requests that can be handled at once like this: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -839,7 +841,7 @@ also use a streaming approach where only small chunks of data have to be kept in memory: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), $handler ); @@ -854,6 +856,10 @@ have full control over consuming the incoming HTTP request body and concurrency settings. See also [streaming incoming request](#streaming-incoming-request) below for more details. +> Changelog v1.5.0: This class has been renamed to `HttpServer` from the + previous `Server` class in order to avoid any ambiguities. + The previous name has been deprecated and should not be used anymore. + ### listen() The `listen(React\Socket\ServerInterface $socket): void` method can be used to @@ -868,10 +874,10 @@ messages. In its most common form, you can attach this to a order to start a plaintext HTTP server like this: ```php -$server = new React\Http\Server($handler); +$http = new React\Http\HttpServer($handler); $socket = new React\Socket\Server('0.0.0.0:8080'); -$server->listen($socket); +$http->listen($socket); ``` See also [hello world server example](examples/51-server-hello-world.php) @@ -894,12 +900,12 @@ using a secure TLS listen address, a certificate file and optional `passphrase` like this: ```php -$server = new React\Http\Server($handler); +$http = new React\Http\HttpServer($handler); $socket = new React\Socket\Server('tls://0.0.0.0:8443', null, array( 'local_cert' => __DIR__ . '/localhost.pem' )); -$server->listen($socket); +$http->listen($socket); ``` See also [hello world HTTPS example](examples/61-server-hello-world-https.php) @@ -907,7 +913,7 @@ for more details. ### Server Request -As seen above, the [`Server`](#server) class is responsible for handling +As seen above, the [`HttpServer`](#httpserver) class is responsible for handling incoming connections and then processing each incoming HTTP request. The request object will be processed once the request has @@ -919,7 +925,7 @@ which in turn extends the and will be passed to the callback function like this. ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $body = "The method of the request is: " . $request->getMethod(); $body .= "The requested path is: " . $request->getUri()->getPath(); @@ -962,7 +968,7 @@ The following parameters are currently available: Set to 'on' if the request used HTTPS, otherwise it won't be set ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; return new React\Http\Message\Response( @@ -987,7 +993,7 @@ The `getQueryParams(): array` method can be used to get the query parameters similiar to the `$_GET` variable. ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $queryParams = $request->getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; @@ -1017,7 +1023,7 @@ See also [server query parameters example](examples/54-server-query-parameter.ph #### Request body -By default, the [`Server`](#server) will buffer and parse the full request body +By default, the [`Server`](#httpserver) will buffer and parse the full request body in memory. This means the given request object includes the parsed request body and any file uploads. @@ -1041,7 +1047,7 @@ By default, this method will only return parsed data for requests using request headers (commonly used for `POST` requests for HTML form submission data). ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $name = $request->getParsedBody()['name'] ?? 'anonymous'; return new React\Http\Message\Response( @@ -1065,7 +1071,7 @@ an XML (`Content-Type: application/xml`) request body (which is commonly used fo `POST`, `PUT` or `PATCH` requests in JSON-based or RESTful/RESTish APIs). ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $data = json_decode((string)$request->getBody()); $name = $data->name ?? 'anonymous'; @@ -1088,7 +1094,7 @@ This array will only be filled when using the `Content-Type: multipart/form-data request header (commonly used for `POST` requests for HTML file uploads). ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $files = $request->getUploadedFiles(); $name = isset($files['avatar']) ? $files['avatar']->getClientFilename() : 'nothing'; @@ -1112,7 +1118,7 @@ This method operates on the buffered request body, i.e. the request body size is always known, even when the request does not specify a `Content-Length` request header or when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. -> Note: The `Server` automatically takes care of handling requests with the +> Note: The `HttpServer` automatically takes care of handling requests with the additional `Expect: 100-continue` request header. When HTTP/1.1 clients want to send a bigger request body, they MAY send only the request headers with an additional `Expect: 100-continue` request header and wait before sending the actual @@ -1161,7 +1167,7 @@ The [ReactPHP `ReadableStreamInterface`](https://github.com/reactphp/stream#read gives you access to the incoming request body as the individual chunks arrive: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $body = $request->getBody(); @@ -1234,7 +1240,7 @@ This method operates on the streaming request body, i.e. the request body size may be unknown (`null`) when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $size = $request->getBody()->getSize(); @@ -1262,7 +1268,7 @@ $server = new React\Http\Server( ); ``` -> Note: The `Server` automatically takes care of handling requests with the +> Note: The `HttpServer` automatically takes care of handling requests with the additional `Expect: 100-continue` request header. When HTTP/1.1 clients want to send a bigger request body, they MAY send only the request headers with an additional `Expect: 100-continue` request header and wait before sending the actual @@ -1307,7 +1313,7 @@ The `getCookieParams(): string[]` method can be used to get all cookies sent with the current request. ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $key = 'react\php'; if (isset($request->getCookieParams()[$key])) { @@ -1344,7 +1350,7 @@ See also [cookie server example](examples/55-server-cookie-handling.php) for mor #### Invalid request -The `Server` class supports both HTTP/1.1 and HTTP/1.0 request messages. +The `HttpServer` class supports both HTTP/1.1 and HTTP/1.0 request messages. If a client sends an invalid request message, uses an invalid HTTP protocol version or sends an invalid `Transfer-Encoding` request header value, the server will automatically send a `400` (Bad Request) HTTP error response @@ -1353,7 +1359,7 @@ On top of this, it will emit an `error` event that can be used for logging purposes like this: ```php -$server->on('error', function (Exception $e) { +$http->on('error', function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -1364,7 +1370,7 @@ valid response object from your request handler function. See also ### Server Response -The callback function passed to the constructor of the [`Server`](#server) is +The callback function passed to the constructor of the [`HttpServer`](#httpserver) is responsible for processing the request and returning a response, which will be delivered to the client. @@ -1379,7 +1385,7 @@ This projects ships a [`Response` class](#response) which implements the In its most simple form, you can use it like this: ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1407,7 +1413,7 @@ To prevent this you SHOULD use a This example shows how such a long-term action could look like: ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { Loop::addTimer(1.5, function() use ($resolve) { $response = new React\Http\Message\Response( @@ -1445,7 +1451,7 @@ Note that other implementations of the may only support strings. ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $stream = new ThroughStream(); $timer = Loop::addPeriodicTimer(0.5, function () use ($stream) { @@ -1537,7 +1543,7 @@ added automatically. This is the most common use case, for example when using a `string` response body like this: ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1556,7 +1562,7 @@ response messages will contain the plain response body. If you know the length of your streaming response body, you MAY want to specify it explicitly like this: ```php -$server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $stream = new ThroughStream(); Loop::addTimer(2.0, function () use ($stream) { @@ -1600,7 +1606,7 @@ On top of this, it will emit an `error` event that can be used for logging purposes like this: ```php -$server->on('error', function (Exception $e) { +$http->on('error', function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; if ($e->getPrevious() !== null) { echo 'Previous: ' . $e->getPrevious()->getMessage() . PHP_EOL; @@ -1626,13 +1632,13 @@ create your own HTTP response message instead. #### Default response headers When a response is returned from the request handler function, it will be -processed by the [`Server`](#server) and then sent back to the client. +processed by the [`HttpServer`](#httpserver) and then sent back to the client. A `Server: ReactPHP/1` response header will be added automatically. You can add a custom `Server` response header like this: ```php -$server = new React\Http\Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1647,7 +1653,7 @@ don't want to expose the underlying server software), you can use an empty string value like this: ```php -$server = new React\Http\Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1662,7 +1668,7 @@ date and time if none is given. You can add a custom `Date` response header like this: ```php -$server = new React\Http\Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1677,7 +1683,7 @@ don't have an appropriate clock to rely on), you can use an empty string value like this: ```php -$server = new React\Http\Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( 200, array( @@ -1687,7 +1693,7 @@ $server = new React\Http\Server(function (ServerRequestInterface $request) { }); ``` -The `Server` class will automatically add the protocol version of the request, +The `HttpServer` class will automatically add the protocol version of the request, so you don't have to. For instance, if the client sends the request using the HTTP/1.1 protocol version, the response message will also use the same protocol version, no matter what version is returned from the request handler function. @@ -1701,7 +1707,7 @@ choice with a `Connection: close` response header. ### Middleware -As documented above, the [`Server`](#server) accepts a single request handler +As documented above, the [`HttpServer`](#httpserver) accepts a single request handler argument that is responsible for processing an incoming HTTP request and then creating and returning an outgoing HTTP response. @@ -1755,12 +1761,12 @@ required to match PHP's request behavior (see below) and otherwise actively encourages [Third-Party Middleware](#third-party-middleware) implementations. In order to use middleware request handlers, simply pass an array with all -callables as defined above to the [`Server`](#server). +callables as defined above to the [`HttpServer`](#httpserver). The following example adds a middleware request handler that adds the current time to the request as a header (`Request-Time`) and a final request handler that always returns a 200 code without a body: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { $request = $request->withHeader('Request-Time', time()); return $next($request); @@ -1784,7 +1790,7 @@ In order to simplify handling both paths, you can simply wrap this in a [`Promise\resolve()`](https://reactphp.org/promise/#resolve) call like this: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { $promise = React\Promise\resolve($next($request)); return $promise->then(function (ResponseInterface $response) { @@ -1800,13 +1806,13 @@ $server = new React\Http\Server( Note that the `$next` middleware request handler may also throw an `Exception` (or return a rejected promise) as described above. The previous example does not catch any exceptions and would thus signal an -error condition to the `Server`. +error condition to the `HttpServer`. Alternatively, you can also catch any `Exception` to implement custom error handling logic (or logging etc.) by wrapping this in a [`Promise`](https://reactphp.org/promise/#promise) like this: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { $promise = new React\Promise\Promise(function ($resolve) use ($next, $request) { $resolve($next($request)); @@ -2439,7 +2445,7 @@ body in memory. Instead, it will represent the request body as a that emit chunks of incoming data as it is received: ```php -$server = new React\Http\Server(array( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $body = $request->getBody(); @@ -2460,7 +2466,7 @@ $server = new React\Http\Server(array( }); }); } -)); +); ``` See also [streaming incoming request](#streaming-incoming-request) @@ -2473,17 +2479,17 @@ to explicitly configure the total number of requests that can be handled at once: ```php -$server = new React\Http\Server(array( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request new React\Http\Middleware\RequestBodyParserMiddleware(), $handler -)); +); ``` > Internally, this class is used as a "marker" to not trigger the default - request buffering behavior in the `Server`. It does not implement any logic + request buffering behavior in the `HttpServer`. It does not implement any logic on its own. #### LimitConcurrentRequestsMiddleware @@ -2506,7 +2512,7 @@ The following example shows how this middleware can be used to ensure no more than 10 handlers will be invoked at once: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\LimitConcurrentRequestsMiddleware(10), $handler ); @@ -2517,7 +2523,7 @@ Similarly, this middleware is often used in combination with the to limit the total number of requests that can be buffered at once: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -2531,7 +2537,7 @@ that can be buffered at once and then ensure the actual request handler only processes one request after another without any concurrency: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -2584,7 +2590,7 @@ the total number of concurrent requests. Usage: ```php -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB @@ -2645,7 +2651,7 @@ $handler = function (Psr\Http\Message\ServerRequestInterface $request) { ); }; -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers new React\Http\Middleware\RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB diff --git a/examples/51-server-hello-world.php b/examples/51-server-hello-world.php index 2cfd5649..50c82396 100644 --- a/examples/51-server-hello-world.php +++ b/examples/51-server-hello-world.php @@ -2,11 +2,10 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new Response( 200, array( @@ -16,7 +15,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/52-server-count-visitors.php b/examples/52-server-count-visitors.php index ae173bbd..1884acb0 100644 --- a/examples/52-server-count-visitors.php +++ b/examples/52-server-count-visitors.php @@ -2,12 +2,11 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; $counter = 0; -$server = new Server(function (ServerRequestInterface $request) use (&$counter) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) use (&$counter) { return new Response( 200, array( @@ -17,7 +16,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/53-server-whatsmyip.php b/examples/53-server-whatsmyip.php index 82f34742..89dad25c 100644 --- a/examples/53-server-whatsmyip.php +++ b/examples/53-server-whatsmyip.php @@ -2,11 +2,10 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; return new Response( @@ -18,7 +17,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/54-server-query-parameter.php b/examples/54-server-query-parameter.php index dbddfbc0..2ab16647 100644 --- a/examples/54-server-query-parameter.php +++ b/examples/54-server-query-parameter.php @@ -2,11 +2,10 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { $queryParams = $request->getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; @@ -25,7 +24,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index 55370c3b..ff89fe1e 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -2,11 +2,10 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { $key = 'react\php'; if (isset($request->getCookieParams()[$key])) { @@ -31,7 +30,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/56-server-sleep.php b/examples/56-server-sleep.php index 9f149c4c..bd2ea694 100644 --- a/examples/56-server-sleep.php +++ b/examples/56-server-sleep.php @@ -3,12 +3,11 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\Message\Response; -use React\Http\Server; use React\Promise\Promise; require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { Loop::addTimer(1.5, function() use ($resolve) { $response = new Response( @@ -23,7 +22,7 @@ }); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/57-server-error-handling.php b/examples/57-server-error-handling.php index 6952a559..f5281c53 100644 --- a/examples/57-server-error-handling.php +++ b/examples/57-server-error-handling.php @@ -2,13 +2,12 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; use React\Promise\Promise; require __DIR__ . '/../vendor/autoload.php'; $count = 0; -$server = new Server(function (ServerRequestInterface $request) use (&$count) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) use (&$count) { return new Promise(function ($resolve, $reject) use (&$count) { $count++; @@ -28,7 +27,7 @@ }); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php index 941ded1f..d965e306 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -3,12 +3,11 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\Message\Response; -use React\Http\Server; use React\Stream\ThroughStream; require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { if ($request->getMethod() !== 'GET' || $request->getUri()->getPath() !== '/') { return new Response(404); } @@ -39,7 +38,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/59-server-json-api.php b/examples/59-server-json-api.php index c1b4d305..c06a9702 100644 --- a/examples/59-server-json-api.php +++ b/examples/59-server-json-api.php @@ -8,11 +8,10 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { if ($request->getHeaderLine('Content-Type') !== 'application/json') { return new Response( 415, // Unsupported Media Type @@ -53,7 +52,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index 5b671618..34f1a8bd 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -2,11 +2,10 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new Response( 200, array( @@ -16,11 +15,11 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$socket = new \React\Socket\SecureServer($socket, null, array( +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SecureServer($socket, null, array( 'local_cert' => isset($argv[2]) ? $argv[2] : __DIR__ . '/localhost.pem' )); -$server->listen($socket); +$http->listen($socket); //$socket->on('error', 'printf'); diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index b1f0d8ee..5a0fdace 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -14,7 +14,6 @@ use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\RequestBodyParserMiddleware; use React\Http\Middleware\StreamingRequestMiddleware; -use React\Http\Server; require __DIR__ . '/../vendor/autoload.php'; @@ -121,7 +120,7 @@ // Note how this example explicitly uses the advanced `StreamingRequestMiddleware` to apply // custom request buffering limits below before running our request handler. -$server = new Server( +$http = new React\Http\HttpServer( new StreamingRequestMiddleware(), new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers, queue otherwise new RequestBodyBufferMiddleware(8 * 1024 * 1024), // 8 MiB max, ignore body otherwise @@ -129,7 +128,7 @@ $handler ); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', null); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index c2416e15..693cf2c0 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -5,7 +5,7 @@ // Note how this example uses the advanced `StreamingRequestMiddleware` to allow streaming // the incoming HTTP request. This very simple example merely counts the size // of the streaming body, it does not otherwise buffer its contents in memory. -$server = new React\Http\Server( +$http = new React\Http\HttpServer( new React\Http\Middleware\StreamingRequestMiddleware(), function (Psr\Http\Message\ServerRequestInterface $request) { $body = $request->getBody(); @@ -42,9 +42,9 @@ function (Psr\Http\Message\ServerRequestInterface $request) { } ); -$server->on('error', 'printf'); +$http->on('error', 'printf'); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index 95c2f411..f6ab5ff5 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -4,18 +4,16 @@ // $ curl -v --proxy http://localhost:8080 http://reactphp.org/ use Psr\Http\Message\RequestInterface; -use React\EventLoop\Factory; use React\Http\Message\Response; -use React\Http\Server; use RingCentral\Psr7; require __DIR__ . '/../vendor/autoload.php'; -// Note how this example uses the `Server` without the `StreamingRequestMiddleware`. +// Note how this example uses the `HttpServer` without the `StreamingRequestMiddleware`. // This means that this proxy buffers the whole request before "processing" it. // As such, this is store-and-forward proxy. This could also use the advanced // `StreamingRequestMiddleware` to forward the incoming request as it comes in. -$server = new Server(function (RequestInterface $request) { +$http = new React\Http\HttpServer(function (RequestInterface $request) { if (strpos($request->getRequestTarget(), '://') === false) { return new Response( 400, @@ -46,7 +44,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/72-server-http-connect-proxy.php b/examples/72-server-http-connect-proxy.php index a1f33983..4399614b 100644 --- a/examples/72-server-http-connect-proxy.php +++ b/examples/72-server-http-connect-proxy.php @@ -5,7 +5,6 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; use React\Socket\Connector; use React\Socket\ConnectionInterface; @@ -13,11 +12,11 @@ $connector = new Connector(); -// Note how this example uses the `Server` without the `StreamingRequestMiddleware`. +// Note how this example uses the `HttpServer` without the `StreamingRequestMiddleware`. // Unlike the plain HTTP proxy, the CONNECT method does not contain a body // and we establish an end-to-end connection over the stream object, so this // doesn't have to store any payload data in memory at all. -$server = new Server(function (ServerRequestInterface $request) use ($connector) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) use ($connector) { if ($request->getMethod() !== 'CONNECT') { return new Response( 405, @@ -51,7 +50,7 @@ function ($e) { ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/81-server-upgrade-echo.php b/examples/81-server-upgrade-echo.php index 6a20181a..cb5d0b0d 100644 --- a/examples/81-server-upgrade-echo.php +++ b/examples/81-server-upgrade-echo.php @@ -20,15 +20,14 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\Message\Response; -use React\Http\Server; use React\Stream\ThroughStream; require __DIR__ . '/../vendor/autoload.php'; -// Note how this example uses the `Server` without the `StreamingRequestMiddleware`. +// Note how this example uses the `HttpServer` without the `StreamingRequestMiddleware`. // The initial incoming request does not contain a body and we upgrade to a // stream object below. -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { if ($request->getHeaderLine('Upgrade') !== 'echo' || $request->getProtocolVersion() === '1.0') { return new Response( 426, @@ -57,7 +56,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/82-server-upgrade-chat.php b/examples/82-server-upgrade-chat.php index 2c6f08a8..22fed927 100644 --- a/examples/82-server-upgrade-chat.php +++ b/examples/82-server-upgrade-chat.php @@ -22,7 +22,6 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\Message\Response; -use React\Http\Server; use React\Stream\CompositeStream; use React\Stream\ThroughStream; @@ -33,10 +32,10 @@ // this means that any Upgraded data will simply be sent back to the client $chat = new ThroughStream(); -// Note how this example uses the `Server` without the `StreamingRequestMiddleware`. +// Note how this example uses the `HttpServer` without the `StreamingRequestMiddleware`. // The initial incoming request does not contain a body and we upgrade to a // stream object below. -$server = new Server(function (ServerRequestInterface $request) use ($chat) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) use ($chat) { if ($request->getHeaderLine('Upgrade') !== 'chat' || $request->getProtocolVersion() === '1.0') { return new Response( 426, @@ -85,7 +84,7 @@ ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index a6b4e9c1..bdc6735a 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -17,7 +17,6 @@ use Evenement\EventEmitter; use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; -use React\Http\Server; use React\Stream\ReadableStreamInterface; use React\Stream\WritableStreamInterface; @@ -91,7 +90,7 @@ public function getSize() } } -$server = new Server(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { switch ($request->getUri()->getPath()) { case '/': return new Response( @@ -123,7 +122,7 @@ public function getSize() ); }); -$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$server->listen($socket); +$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/src/Server.php b/src/HttpServer.php similarity index 93% rename from src/Server.php rename to src/HttpServer.php index c2e3f5a6..582ea380 100644 --- a/src/Server.php +++ b/src/HttpServer.php @@ -15,7 +15,7 @@ use React\Socket\ServerInterface; /** - * The `React\Http\Server` class is responsible for handling incoming connections and then + * The `React\Http\HttpServer` class is responsible for handling incoming connections and then * processing each incoming HTTP request. * * When a complete HTTP request has been received, it will invoke the given @@ -24,7 +24,7 @@ * object and expects a [response](#server-response) object in return: * * ```php - * $server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) { + * $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { * return new React\Http\Message\Response( * 200, * array( @@ -49,7 +49,7 @@ * This value SHOULD NOT be given unless you're sure you want to explicitly use a * given event loop instance. * - * In order to start listening for any incoming connections, the `Server` needs + * In order to start listening for any incoming connections, the `HttpServer` needs * to be attached to an instance of * [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) * through the [`listen()`](#listen) method as described in the following @@ -58,17 +58,17 @@ * to start a plaintext HTTP server like this: * * ```php - * $server = new React\Http\Server($handler); + * $http = new React\Http\HttpServer($handler); * * $socket = new React\Socket\Server('0.0.0.0:8080'); - * $server->listen($socket); + * $http->listen($socket); * ``` * * See also the [`listen()`](#listen) method and * [hello world server example](../examples/51-server-hello-world.php) * for more details. * - * By default, the `Server` buffers and parses the complete incoming HTTP + * By default, the `HttpServer` buffers and parses the complete incoming HTTP * request in memory. It will invoke the given request handler function when the * complete request headers and request body has been received. This means the * [request](#server-request) object passed to your request handler function will be @@ -121,14 +121,14 @@ * or explicitly limit concurrency. * * In order to override the above buffering defaults, you can configure the - * `Server` explicitly. You can use the + * `HttpServer` explicitly. You can use the * [`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and * [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) * to explicitly configure the total number of requests that can be handled at * once like this: * * ```php - * $server = new React\Http\Server( + * $http = new React\Http\HttpServer( * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -153,7 +153,7 @@ * in memory: * * ```php - * $server = new React\Http\Server( + * $http = new React\Http\HttpServer( * new React\Http\Middleware\StreamingRequestMiddleware(), * $handler * ); @@ -167,8 +167,12 @@ * have full control over consuming the incoming HTTP request body and * concurrency settings. See also [streaming incoming request](#streaming-incoming-request) * below for more details. + * + * > Changelog v1.5.0: This class has been renamed to `HttpServer` from the + * previous `Server` class in order to avoid any ambiguities. + * The previous name has been deprecated and should not be used anymore. */ -final class Server extends EventEmitter +final class HttpServer extends EventEmitter { /** * The maximum buffer size used for each request. @@ -263,10 +267,10 @@ public function __construct($requestHandlerOrLoop) * order to start a plaintext HTTP server like this: * * ```php - * $server = new React\Http\Server($handler); + * $http = new React\Http\HttpServer($handler); * * $socket = new React\Socket\Server(8080); - * $server->listen($socket); + * $http->listen($socket); * ``` * * See also [hello world server example](../examples/51-server-hello-world.php) @@ -289,12 +293,12 @@ public function __construct($requestHandlerOrLoop) * `passphrase` like this: * * ```php - * $server = new React\Http\Server($handler); + * $http = new React\Http\HttpServer($handler); * * $socket = new React\Socket\Server('tls://0.0.0.0:8443', null, array( * 'local_cert' => __DIR__ . '/localhost.pem' * )); - * $server->listen($socket); + * $http->listen($socket); * ``` * * See also [hello world HTTPS example](../examples/61-server-hello-world-https.php) diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index e20ddf48..95e0e4c6 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -20,7 +20,7 @@ * The internal `StreamingServer` class is responsible for handling incoming connections and then * processing each incoming HTTP request. * - * Unlike the [`Server`](#server) class, it does not buffer and parse the incoming + * Unlike the [`HttpServer`](#server) class, it does not buffer and parse the incoming * HTTP request body by default. This means that the request handler will be * invoked with a streaming request body. Once the request headers have been * received, it will invoke the request handler function. This request handler @@ -63,7 +63,7 @@ * See also the [`listen()`](#listen) method and the [first example](examples) for more details. * * The `StreamingServer` class is considered advanced usage and unless you know - * what you're doing, you're recommended to use the [`Server`](#server) class + * what you're doing, you're recommended to use the [`HttpServer`](#httpserver) class * instead. The `StreamingServer` class is specifically designed to help with * more advanced use cases where you want to have full control over consuming * the incoming HTTP request body and concurrency settings. @@ -75,7 +75,7 @@ * handler function may not be fully compatible with PSR-7. See also * [streaming request](#streaming-request) below for more details. * - * @see \React\Http\Server + * @see \React\Http\HttpServer * @see \React\Http\Message\Response * @see self::listen() * @internal @@ -130,7 +130,7 @@ public function __construct(LoopInterface $loop, $requestHandler) * Starts listening for HTTP requests on the given socket server instance * * @param ServerInterface $socket - * @see \React\Http\Server::listen() + * @see \React\Http\HttpServer::listen() */ public function listen(ServerInterface $socket) { diff --git a/src/Middleware/LimitConcurrentRequestsMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php index cc1dfc42..53338100 100644 --- a/src/Middleware/LimitConcurrentRequestsMiddleware.php +++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -29,7 +29,7 @@ * than 10 handlers will be invoked at once: * * ```php - * $server = new React\Http\Server( + * $http = new React\Http\HttpServer( * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(10), * $handler @@ -41,7 +41,7 @@ * to limit the total number of requests that can be buffered at once: * * ```php - * $server = new React\Http\Server( + * $http = new React\Http\HttpServer( * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request @@ -55,7 +55,7 @@ * processes one request after another without any concurrency: * * ```php - * $server = new React\Http\Server( + * $http = new React\Http\HttpServer( * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request diff --git a/src/Middleware/StreamingRequestMiddleware.php b/src/Middleware/StreamingRequestMiddleware.php index a68454f6..6ab74b71 100644 --- a/src/Middleware/StreamingRequestMiddleware.php +++ b/src/Middleware/StreamingRequestMiddleware.php @@ -13,7 +13,7 @@ * that emit chunks of incoming data as it is received: * * ```php - * $server = new React\Http\Server(array( + * $http = new React\Http\HttpServer( * new React\Http\Middleware\StreamingRequestMiddleware(), * function (Psr\Http\Message\ServerRequestInterface $request) { * $body = $request->getBody(); @@ -34,7 +34,7 @@ * }); * }); * } - * )); + * ); * ``` * * See also [streaming incoming request](../../README.md#streaming-incoming-request) @@ -47,17 +47,17 @@ * once: * * ```php - * $server = new React\Http\Server(array( + * $http = new React\Http\HttpServer( * new React\Http\Middleware\StreamingRequestMiddleware(), * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request * new React\Http\Middleware\RequestBodyParserMiddleware(), * $handler - * )); + * ); * ``` * * > Internally, this class is used as a "marker" to not trigger the default - * request buffering behavior in the `Server`. It does not implement any logic + * request buffering behavior in the `HttpServer`. It does not implement any logic * on its own. */ final class StreamingRequestMiddleware diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index f5b7f324..edb7f40b 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -7,10 +7,10 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Factory; use React\Http\Browser; +use React\Http\HttpServer; use React\Http\Message\ResponseException; use React\Http\Middleware\StreamingRequestMiddleware; use React\Http\Message\Response; -use React\Http\Server; use React\Promise\Promise; use React\Promise\Stream; use React\Socket\Connector; @@ -32,7 +32,7 @@ public function setUpBrowserAndServer() $this->loop = $loop = Factory::create(); $this->browser = new Browser($this->loop); - $server = new Server($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use ($loop) { + $http = new HttpServer($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use ($loop) { $path = $request->getUri()->getPath(); $headers = array(); @@ -142,7 +142,7 @@ public function setUpBrowserAndServer() var_dump($path); }); $socket = new \React\Socket\Server(0, $this->loop); - $server->listen($socket); + $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; } @@ -573,11 +573,11 @@ public function testPostStreamKnownLength() */ public function testPostStreamWillStartSendingRequestEvenWhenBodyDoesNotEmitData() { - $server = new Server($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { + $http = new HttpServer($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { return new Response(200); }); $socket = new \React\Socket\Server(0, $this->loop); - $server->listen($socket); + $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; @@ -600,7 +600,7 @@ public function testPostStreamClosed() public function testSendsHttp11ByDefault() { - $server = new Server($this->loop, function (ServerRequestInterface $request) { + $http = new HttpServer($this->loop, function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -608,7 +608,7 @@ public function testSendsHttp11ByDefault() ); }); $socket = new \React\Socket\Server(0, $this->loop); - $server->listen($socket); + $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; @@ -620,7 +620,7 @@ public function testSendsHttp11ByDefault() public function testSendsExplicitHttp10Request() { - $server = new Server($this->loop, function (ServerRequestInterface $request) { + $http = new HttpServer($this->loop, function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -628,7 +628,7 @@ public function testSendsExplicitHttp10Request() ); }); $socket = new \React\Socket\Server(0, $this->loop); - $server->listen($socket); + $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; diff --git a/tests/FunctionalServerTest.php b/tests/FunctionalHttpServerTest.php similarity index 90% rename from tests/FunctionalServerTest.php rename to tests/FunctionalHttpServerTest.php index 41cf31db..f7aa8392 100644 --- a/tests/FunctionalServerTest.php +++ b/tests/FunctionalHttpServerTest.php @@ -2,36 +2,36 @@ namespace React\Tests\Http; +use Clue\React\Block; +use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ServerRequestInterface; +use React\EventLoop\Factory; +use React\Http\HttpServer; +use React\Http\Message\Response; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; -use React\Http\Message\Response; -use React\Http\Server; +use React\Http\Middleware\StreamingRequestMiddleware; use React\Socket\Server as Socket; -use React\EventLoop\Factory; -use Psr\Http\Message\RequestInterface; use React\Socket\Connector; use React\Socket\ConnectionInterface; -use Clue\React\Block; use React\Socket\SecureServer; use React\Promise; use React\Promise\Stream; use React\Stream\ThroughStream; -use React\Http\Middleware\StreamingRequestMiddleware; -class FunctionalServerTest extends TestCase +class FunctionalHttpServerTest extends TestCase { public function testPlainHttpOnRandomPort() { $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); @@ -52,7 +52,7 @@ public function testPlainHttpOnRandomPortWithSingleRequestHandlerArray() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server( + $http = new HttpServer( $loop, function () { return new Response(404); @@ -60,7 +60,7 @@ function () { ); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); @@ -80,12 +80,12 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -106,12 +106,12 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: localhost:1000\r\n\r\n"); @@ -138,7 +138,7 @@ public function testSecureHttpsOnRandomPort() 'tls' => array('verify_peer' => false) )); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -146,7 +146,7 @@ public function testSecureHttpsOnRandomPort() $socket = new SecureServer($socket, $loop, array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' )); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); @@ -170,7 +170,7 @@ public function testSecureHttpsReturnsData() $loop = Factory::create(); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response( 200, array(), @@ -182,7 +182,7 @@ public function testSecureHttpsReturnsData() $socket = new SecureServer($socket, $loop, array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' )); - $server->listen($socket); + $http->listen($socket); $connector = new Connector($loop, array( 'tls' => array('verify_peer' => false) @@ -214,7 +214,7 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() 'tls' => array('verify_peer' => false) )); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -222,7 +222,7 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() $socket = new SecureServer($socket, $loop, array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' )); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -248,11 +248,11 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() } $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"); @@ -278,11 +278,11 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort } $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -317,11 +317,11 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() 'tls' => array('verify_peer' => false) )); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"); @@ -356,11 +356,11 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() 'tls' => array('verify_peer' => false) )); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -386,11 +386,11 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() } $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); @@ -425,11 +425,11 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() 'tls' => array('verify_peer' => false) )); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri() . 'x' . $request->getHeaderLine('Host')); }); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); @@ -453,12 +453,12 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() $stream = new ThroughStream(); $stream->close(); - $server = new Server($loop, function (RequestInterface $request) use ($stream) { + $http = new HttpServer($loop, function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -480,7 +480,7 @@ public function testRequestHandlerWithStreamingRequestWillReceiveCloseEventIfCon $connector = new Connector($loop); $once = $this->expectCallableOnce(); - $server = new Server( + $http = new HttpServer( $loop, new StreamingRequestMiddleware(), function (RequestInterface $request) use ($once) { @@ -489,7 +489,7 @@ function (RequestInterface $request) use ($once) { ); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { $conn->write("GET / HTTP/1.0\r\nContent-Length: 100\r\n\r\n"); @@ -511,7 +511,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileS $stream = new ThroughStream(); - $server = new Server( + $http = new HttpServer( $loop, new StreamingRequestMiddleware(), function (RequestInterface $request) use ($stream) { @@ -520,7 +520,7 @@ function (RequestInterface $request) use ($stream) { ); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { $conn->write("GET / HTTP/1.0\r\nContent-Length: 100\r\n\r\n"); @@ -545,12 +545,12 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() $stream = new ThroughStream(); - $server = new Server($loop, function (RequestInterface $request) use ($stream) { + $http = new HttpServer($loop, function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -573,7 +573,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) use ($loop) { + $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -584,7 +584,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.1\r\nHost: example.com:80\r\nUpgrade: echo\r\n\r\n"); @@ -610,7 +610,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) use ($loop) { + $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -621,7 +621,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("POST / HTTP/1.1\r\nHost: example.com:80\r\nUpgrade: echo\r\nContent-Length: 3\r\n\r\n"); @@ -648,7 +648,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) use ($loop) { + $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -659,7 +659,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\nConnection: close\r\n\r\n"); @@ -685,7 +685,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) use ($loop) { + $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); $loop->addTimer(0.1, function () use ($stream) { @@ -700,7 +700,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\nConnection: close\r\n\r\n"); @@ -726,7 +726,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server($loop, function (RequestInterface $request) { + $http = new HttpServer($loop, function (RequestInterface $request) { $stream = new ThroughStream(); $stream->close(); @@ -734,7 +734,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() }); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\nConnection: close\r\n\r\n"); @@ -760,7 +760,7 @@ public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing() $loop = Factory::create(); $connector = new Connector($loop); - $server = new Server( + $http = new HttpServer( $loop, new LimitConcurrentRequestsMiddleware(5), new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB @@ -777,7 +777,7 @@ function (ServerRequestInterface $request) { ); $socket = new Socket(0, $loop); - $server->listen($socket); + $http->listen($socket); $result = array(); for ($i = 0; $i < 6; $i++) { diff --git a/tests/ServerTest.php b/tests/HttpServerTest.php similarity index 81% rename from tests/ServerTest.php rename to tests/HttpServerTest.php index 6cb1b9b5..9a0a789a 100644 --- a/tests/ServerTest.php +++ b/tests/HttpServerTest.php @@ -2,17 +2,17 @@ namespace React\Tests\Http; -use React\EventLoop\Factory; -use React\Http\Server; -use Psr\Http\Message\ServerRequestInterface; -use React\Promise\Deferred; use Clue\React\Block; -use React\Promise; +use Psr\Http\Message\ServerRequestInterface; +use React\EventLoop\Factory; +use React\Http\HttpServer; +use React\Http\Io\IniUtil; use React\Http\Middleware\StreamingRequestMiddleware; +use React\Promise; +use React\Promise\Deferred; use React\Stream\ReadableStreamInterface; -use React\Http\Io\IniUtil; -final class ServerTest extends TestCase +final class HttpServerTest extends TestCase { private $connection; private $socket; @@ -48,11 +48,11 @@ public function setUpConnectionMockAndSocket() public function testConstructWithoutLoopAssignsLoopAutomatically() { - $server = new Server(function () { }); + $http = new HttpServer(function () { }); - $ref = new \ReflectionProperty($server, 'streamingServer'); + $ref = new \ReflectionProperty($http, 'streamingServer'); $ref->setAccessible(true); - $streamingServer = $ref->getValue($server); + $streamingServer = $ref->getValue($http); $ref = new \ReflectionProperty($streamingServer, 'loop'); $ref->setAccessible(true); @@ -64,17 +64,17 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() public function testInvalidCallbackFunctionLeadsToException() { $this->setExpectedException('InvalidArgumentException'); - new Server(Factory::create(), 'invalid'); + new HttpServer('invalid'); } public function testSimpleRequestCallsRequestHandlerOnce() { $called = null; - $server = new Server(Factory::create(), function (ServerRequestInterface $request) use (&$called) { + $http = new HttpServer(function (ServerRequestInterface $request) use (&$called) { ++$called; }); - $server->listen($this->socket); + $http->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); @@ -87,9 +87,9 @@ public function testSimpleRequestCallsRequestHandlerOnce() public function testSimpleRequestCallsArrayRequestHandlerOnce() { $this->called = null; - $server = new Server(Factory::create(), array($this, 'helperCallableOnce')); + $http = new HttpServer(array($this, 'helperCallableOnce')); - $server->listen($this->socket); + $http->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); @@ -104,8 +104,7 @@ public function helperCallableOnce() public function testSimpleRequestWithMiddlewareArrayProcessesMiddlewareStack() { $called = null; - $server = new Server( - Factory::create(), + $http = new HttpServer( function (ServerRequestInterface $request, $next) use (&$called) { $called = 'before'; $ret = $next($request->withHeader('Demo', 'ok')); @@ -118,7 +117,7 @@ function (ServerRequestInterface $request) use (&$called) { } ); - $server->listen($this->socket); + $http->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); @@ -129,11 +128,11 @@ public function testPostFormData() { $loop = Factory::create(); $deferred = new Deferred(); - $server = new Server($loop, function (ServerRequestInterface $request) use ($deferred) { + $http = new HttpServer($loop, function (ServerRequestInterface $request) use ($deferred) { $deferred->resolve($request); }); - $server->listen($this->socket); + $http->listen($this->socket); $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")); @@ -158,11 +157,11 @@ public function testPostFileUpload() { $loop = Factory::create(); $deferred = new Deferred(); - $server = new Server($loop, function (ServerRequestInterface $request) use ($deferred) { + $http = new HttpServer($loop, function (ServerRequestInterface $request) use ($deferred) { $deferred->resolve($request); }); - $server->listen($this->socket); + $http->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $connection = $this->connection; @@ -202,11 +201,11 @@ public function testPostJsonWillNotBeParsedByDefault() { $loop = Factory::create(); $deferred = new Deferred(); - $server = new Server($loop, function (ServerRequestInterface $request) use ($deferred) { + $http = new HttpServer($loop, function (ServerRequestInterface $request) use ($deferred) { $deferred->resolve($request); }); - $server->listen($this->socket); + $http->listen($this->socket); $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]")); @@ -227,11 +226,11 @@ public function testPostJsonWillNotBeParsedByDefault() public function testServerReceivesBufferedRequestByDefault() { $streaming = null; - $server = new Server(Factory::create(), function (ServerRequestInterface $request) use (&$streaming) { + $http = new HttpServer(function (ServerRequestInterface $request) use (&$streaming) { $streaming = $request->getBody() instanceof ReadableStreamInterface; }); - $server->listen($this->socket); + $http->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); @@ -241,15 +240,14 @@ public function testServerReceivesBufferedRequestByDefault() public function testServerWithStreamingRequestMiddlewareReceivesStreamingRequest() { $streaming = null; - $server = new Server( - Factory::create(), + $http = new HttpServer( new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use (&$streaming) { $streaming = $request->getBody() instanceof ReadableStreamInterface; } ); - $server->listen($this->socket); + $http->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); @@ -260,14 +258,14 @@ public function testForwardErrors() { $exception = new \Exception(); $capturedException = null; - $server = new Server(Factory::create(), function () use ($exception) { + $http = new HttpServer(function () use ($exception) { return Promise\reject($exception); }); - $server->on('error', function ($error) use (&$capturedException) { + $http->on('error', function ($error) use (&$capturedException) { $capturedException = $error; }); - $server->listen($this->socket); + $http->listen($this->socket); $this->socket->emit('connection', array($this->connection)); $data = $this->createPostFileUploadRequest(); @@ -328,36 +326,36 @@ public function provideIniSettingsForConcurrency() */ public function testServerConcurrency($memory_limit, $post_max_size, $expectedConcurrency) { - $server = new Server(Factory::create(), function () { }); + $http = new HttpServer(function () { }); - $ref = new \ReflectionMethod($server, 'getConcurrentRequestsLimit'); + $ref = new \ReflectionMethod($http, 'getConcurrentRequestsLimit'); $ref->setAccessible(true); - $value = $ref->invoke($server, $memory_limit, $post_max_size); + $value = $ref->invoke($http, $memory_limit, $post_max_size); $this->assertEquals($expectedConcurrency, $value); } public function testServerGetPostMaxSizeReturnsSizeFromGivenIniSetting() { - $server = new Server(Factory::create(), function () { }); + $http = new HttpServer(function () { }); - $ref = new \ReflectionMethod($server, 'getMaxRequestSize'); + $ref = new \ReflectionMethod($http, 'getMaxRequestSize'); $ref->setAccessible(true); - $value = $ref->invoke($server, '1k'); + $value = $ref->invoke($http, '1k'); $this->assertEquals(1024, $value); } public function testServerGetPostMaxSizeReturnsSizeCappedFromGivenIniSetting() { - $server = new Server(Factory::create(), function () { }); + $http = new HttpServer(function () { }); - $ref = new \ReflectionMethod($server, 'getMaxRequestSize'); + $ref = new \ReflectionMethod($http, 'getMaxRequestSize'); $ref->setAccessible(true); - $value = $ref->invoke($server, '1M'); + $value = $ref->invoke($http, '1M'); $this->assertEquals(64 * 1024, $value); } @@ -368,12 +366,12 @@ public function testServerGetPostMaxSizeFromIniIsCapped() $this->markTestSkipped(); } - $server = new Server(Factory::create(), function () { }); + $http = new HttpServer(function () { }); - $ref = new \ReflectionMethod($server, 'getMaxRequestSize'); + $ref = new \ReflectionMethod($http, 'getMaxRequestSize'); $ref->setAccessible(true); - $value = $ref->invoke($server); + $value = $ref->invoke($http); $this->assertEquals(64 * 1024, $value); } @@ -383,14 +381,14 @@ public function testConstructServerWithUnlimitedMemoryLimitDoesNotLimitConcurren $old = ini_get('memory_limit'); ini_set('memory_limit', '-1'); - $server = new Server(Factory::create(), function () { }); + $http = new HttpServer(function () { }); ini_set('memory_limit', $old); - $ref = new \ReflectionProperty($server, 'streamingServer'); + $ref = new \ReflectionProperty($http, 'streamingServer'); $ref->setAccessible(true); - $streamingServer = $ref->getValue($server); + $streamingServer = $ref->getValue($http); $ref = new \ReflectionProperty($streamingServer, 'callback'); $ref->setAccessible(true); @@ -411,14 +409,14 @@ public function testConstructServerWithMemoryLimitDoesLimitConcurrency() $old = ini_get('memory_limit'); ini_set('memory_limit', '100M'); - $server = new Server(Factory::create(), function () { }); + $http = new HttpServer(function () { }); ini_set('memory_limit', $old); - $ref = new \ReflectionProperty($server, 'streamingServer'); + $ref = new \ReflectionProperty($http, 'streamingServer'); $ref->setAccessible(true); - $streamingServer = $ref->getValue($server); + $streamingServer = $ref->getValue($http); $ref = new \ReflectionProperty($streamingServer, 'callback'); $ref->setAccessible(true); From 255e89173a6a1636fbcd19d472ea7d492cbbc859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 25 Jul 2021 20:13:37 +0200 Subject: [PATCH 065/152] Add deprecated `Server` alias for new `HttpServer` class to ensure BC --- src/Server.php | 18 ++++++++++++++++++ tests/ServerTest.php | 15 +++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/Server.php create mode 100644 tests/ServerTest.php diff --git a/src/Server.php b/src/Server.php new file mode 100644 index 00000000..9bb9cf7f --- /dev/null +++ b/src/Server.php @@ -0,0 +1,18 @@ +assertInstanceOf('React\Http\HttpServer', $http); + } +} From 12a4946268ebe61f9a15de5acc495351778e70b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 27 Jul 2021 10:37:57 +0200 Subject: [PATCH 066/152] Update `Browser` signature to take `$connector` as first argument --- README.md | 31 ++++-- examples/11-client-http-connect-proxy.php | 3 +- examples/12-client-socks-proxy.php | 3 +- examples/13-client-ssh-proxy.php | 3 +- examples/14-client-unix-domain-sockets.php | 2 +- src/Browser.php | 41 ++++++-- tests/BrowserTest.php | 114 ++++++++++++++++++++- tests/FunctionalBrowserTest.php | 6 +- 8 files changed, 172 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 126fdcaf..e82307cf 100644 --- a/README.md +++ b/README.md @@ -259,7 +259,6 @@ like this: ```php $browser = new React\Http\Browser( - null, new React\Socket\Connector( null, array( @@ -610,7 +609,7 @@ $connector = new React\Socket\Connector(null, array( 'dns' => false )); -$browser = new React\Http\Browser(null, $connector); +$browser = new React\Http\Browser($connector); ``` See also the [HTTP CONNECT proxy example](examples/11-client-http-connect-proxy.php). @@ -637,7 +636,7 @@ $connector = new React\Socket\Connector(null, array( 'dns' => false )); -$browser = new React\Http\Browser(null, $connector); +$browser = new React\Http\Browser($connector); ``` See also the [SOCKS proxy example](examples/12-client-socks-proxy.php). @@ -666,7 +665,7 @@ $connector = new React\Socket\Connector(null, array( 'dns' => false )); -$browser = new React\Http\Browser(null, $connector); +$browser = new React\Http\Browser($connector); ``` See also the [SSH proxy example](examples/13-client-ssh-proxy.php). @@ -687,7 +686,7 @@ $connector = new React\Socket\FixedUriConnector( new React\Socket\UnixConnector() ); -$browser = new Browser(null, $connector); +$browser = new React\Http\Browser($connector); $client->get('http://localhost/info')->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); @@ -1869,11 +1868,15 @@ and keeps track of pending incoming HTTP responses. $browser = new React\Http\Browser(); ``` -This class takes an optional `LoopInterface|null $loop` parameter that can be used to -pass the event loop instance to use for this object. You can use a `null` value -here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). -This value SHOULD NOT be given unless you're sure you want to explicitly use a -given event loop instance. +This class takes two optional arguments for more advanced usage: + +```php +// constructor signature as of v1.5.0 +$browser = new React\Http\Browser(?ConnectorInterface $connector = null, ?LoopInterface $loop = null); + +// legacy constructor signature before v1.5.0 +$browser = new React\Http\Browser(?LoopInterface $loop = null, ?ConnectorInterface $connector = null); +``` If you need custom connector settings (DNS resolution, TLS parameters, timeouts, proxy servers etc.), you can explicitly pass a custom instance of the @@ -1891,9 +1894,15 @@ $connector = new React\Socket\Connector(null, array( ) )); -$browser = new React\Http\Browser(null, $connector); +$browser = new React\Http\Browser($connector); ``` +This class takes an optional `LoopInterface|null $loop` parameter that can be used to +pass the event loop instance to use for this object. You can use a `null` value +here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). +This value SHOULD NOT be given unless you're sure you want to explicitly use a +given event loop instance. + > Note that the browser class is final and shouldn't be extended, it is likely to be marked final in a future release. #### get() diff --git a/examples/11-client-http-connect-proxy.php b/examples/11-client-http-connect-proxy.php index afa22e83..ca7104aa 100644 --- a/examples/11-client-http-connect-proxy.php +++ b/examples/11-client-http-connect-proxy.php @@ -21,7 +21,8 @@ 'tcp' => $proxy, 'dns' => false )); -$browser = new Browser(null, $connector); + +$browser = new Browser($connector); // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { diff --git a/examples/12-client-socks-proxy.php b/examples/12-client-socks-proxy.php index ec2375c5..5801b138 100644 --- a/examples/12-client-socks-proxy.php +++ b/examples/12-client-socks-proxy.php @@ -18,7 +18,8 @@ 'tcp' => $proxy, 'dns' => false )); -$browser = new Browser(null, $connector); + +$browser = new Browser($connector); // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { diff --git a/examples/13-client-ssh-proxy.php b/examples/13-client-ssh-proxy.php index d4c8dcea..764fb30f 100644 --- a/examples/13-client-ssh-proxy.php +++ b/examples/13-client-ssh-proxy.php @@ -17,7 +17,8 @@ 'tcp' => $proxy, 'dns' => false )); -$browser = new Browser(null, $connector); + +$browser = new Browser($connector); // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { diff --git a/examples/14-client-unix-domain-sockets.php b/examples/14-client-unix-domain-sockets.php index f60dd9f7..36bfecda 100644 --- a/examples/14-client-unix-domain-sockets.php +++ b/examples/14-client-unix-domain-sockets.php @@ -14,7 +14,7 @@ new UnixConnector() ); -$browser = new Browser(null, $connector); +$browser = new Browser($connector); // demo fetching HTTP headers (or bail out otherwise) $browser->get('http://localhost/info')->then(function (ResponseInterface $response) { diff --git a/src/Browser.php b/src/Browser.php index ed83689b..11519094 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -32,11 +32,15 @@ class Browser * $browser = new React\Http\Browser(); * ``` * - * This class takes an optional `LoopInterface|null $loop` parameter that can be used to - * pass the event loop instance to use for this object. You can use a `null` value - * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). - * This value SHOULD NOT be given unless you're sure you want to explicitly use a - * given event loop instance. + * This class takes two optional arguments for more advanced usage: + * + * ```php + * // constructor signature as of v1.5.0 + * $browser = new React\Http\Browser(?ConnectorInterface $connector = null, ?LoopInterface $loop = null); + * + * // legacy constructor signature before v1.5.0 + * $browser = new React\Http\Browser(?LoopInterface $loop = null, ?ConnectorInterface $connector = null); + * ``` * * If you need custom connector settings (DNS resolution, TLS parameters, timeouts, * proxy servers etc.), you can explicitly pass a custom instance of the @@ -54,15 +58,32 @@ class Browser * ) * )); * - * $browser = new React\Http\Browser(null, $connector); + * $browser = new React\Http\Browser($connector); * ``` * - * @param ?LoopInterface $loop - * @param ?ConnectorInterface $connector [optional] Connector to use. - * Should be `null` in order to use default Connector. + * This class takes an optional `LoopInterface|null $loop` parameter that can be used to + * pass the event loop instance to use for this object. You can use a `null` value + * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). + * This value SHOULD NOT be given unless you're sure you want to explicitly use a + * given event loop instance. + * + * @param null|ConnectorInterface|LoopInterface $connector + * @param null|LoopInterface|ConnectorInterface $loop + * @throws \InvalidArgumentException for invalid arguments */ - public function __construct(LoopInterface $loop = null, ConnectorInterface $connector = null) + public function __construct($connector = null, $loop = null) { + // swap arguments for legacy constructor signature + if (($connector instanceof LoopInterface || $connector === null) && ($loop instanceof ConnectorInterface || $loop === null)) { + $swap = $loop; + $loop = $connector; + $connector = $swap; + } + + if (($connector !== null && !$connector instanceof ConnectorInterface) || ($loop !== null && !$loop instanceof LoopInterface)) { + throw new \InvalidArgumentException('Expected "?ConnectorInterface $connector" and "?LoopInterface $loop" arguments'); + } + $loop = $loop ?: Loop::get(); $this->transaction = new Transaction( Sender::createFromLoop($loop, $connector), diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index 5df2b837..39be453a 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -3,8 +3,8 @@ namespace React\Tests\Http; use Clue\React\Block; -use React\Http\Browser; use Psr\Http\Message\RequestInterface; +use React\Http\Browser; use React\Promise\Promise; use RingCentral\Psr7\Uri; @@ -21,7 +21,7 @@ public function setUpBrowser() { $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $this->sender = $this->getMockBuilder('React\Http\Io\Transaction')->disableOriginalConstructor()->getMock(); - $this->browser = new Browser($this->loop); + $this->browser = new Browser(null, $this->loop); $ref = new \ReflectionProperty($this->browser, 'transaction'); $ref->setAccessible(true); @@ -43,6 +43,114 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); } + public function testConstructWithConnectorAssignsGivenConnector() + { + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + + $browser = new Browser($connector); + + $ref = new \ReflectionProperty($browser, 'transaction'); + $ref->setAccessible(true); + $transaction = $ref->getValue($browser); + + $ref = new \ReflectionProperty($transaction, 'sender'); + $ref->setAccessible(true); + $sender = $ref->getValue($transaction); + + $ref = new \ReflectionProperty($sender, 'http'); + $ref->setAccessible(true); + $client = $ref->getValue($sender); + + $ref = new \ReflectionProperty($client, 'connector'); + $ref->setAccessible(true); + $ret = $ref->getValue($client); + + $this->assertSame($connector, $ret); + } + + public function testConstructWithConnectorWithLegacySignatureAssignsGivenConnector() + { + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + + $browser = new Browser(null, $connector); + + $ref = new \ReflectionProperty($browser, 'transaction'); + $ref->setAccessible(true); + $transaction = $ref->getValue($browser); + + $ref = new \ReflectionProperty($transaction, 'sender'); + $ref->setAccessible(true); + $sender = $ref->getValue($transaction); + + $ref = new \ReflectionProperty($sender, 'http'); + $ref->setAccessible(true); + $client = $ref->getValue($sender); + + $ref = new \ReflectionProperty($client, 'connector'); + $ref->setAccessible(true); + $ret = $ref->getValue($client); + + $this->assertSame($connector, $ret); + } + + public function testConstructWithLoopAssignsGivenLoop() + { + $browser = new Browser(null, $this->loop); + + $ref = new \ReflectionProperty($browser, 'transaction'); + $ref->setAccessible(true); + $transaction = $ref->getValue($browser); + + $ref = new \ReflectionProperty($transaction, 'loop'); + $ref->setAccessible(true); + $loop = $ref->getValue($transaction); + + $this->assertSame($this->loop, $loop); + } + + public function testConstructWithLoopWithLegacySignatureAssignsGivenLoop() + { + $browser = new Browser($this->loop); + + $ref = new \ReflectionProperty($browser, 'transaction'); + $ref->setAccessible(true); + $transaction = $ref->getValue($browser); + + $ref = new \ReflectionProperty($transaction, 'loop'); + $ref->setAccessible(true); + $loop = $ref->getValue($transaction); + + $this->assertSame($this->loop, $loop); + } + + public function testConstructWithInvalidConnectorThrows() + { + $this->setExpectedException('InvalidArgumentException'); + new Browser('foo'); + } + + public function testConstructWithInvalidLoopThrows() + { + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + + $this->setExpectedException('InvalidArgumentException'); + new Browser($connector, 'foo'); + } + + public function testConstructWithConnectorTwiceThrows() + { + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + + $this->setExpectedException('InvalidArgumentException'); + new Browser($connector, $connector); + } + + public function testConstructWithLoopTwiceThrows() + { + $this->setExpectedException('InvalidArgumentException'); + new Browser($this->loop, $this->loop); + } + public function testGetSendsGetRequest() { $that = $this; @@ -390,7 +498,7 @@ public function testCancelGetRequestShouldCancelUnderlyingSocketConnection() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->with('example.com:80')->willReturn($pending); - $this->browser = new Browser($this->loop, $connector); + $this->browser = new Browser($connector, $this->loop); $promise = $this->browser->get('http://example.com/'); $promise->cancel(); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index f5b7f324..666bd9a2 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -30,7 +30,7 @@ class FunctionalBrowserTest extends TestCase public function setUpBrowserAndServer() { $this->loop = $loop = Factory::create(); - $this->browser = new Browser($this->loop); + $this->browser = new Browser(null, $this->loop); $server = new Server($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use ($loop) { $path = $request->getUri()->getPath(); @@ -398,7 +398,7 @@ public function testVerifyPeerEnabledForBadSslRejects() ) )); - $browser = new Browser($this->loop, $connector); + $browser = new Browser($connector, $this->loop); $this->setExpectedException('RuntimeException'); Block\await($browser->get('https://self-signed.badssl.com/'), $this->loop); @@ -420,7 +420,7 @@ public function testVerifyPeerDisabledForBadSslResolves() ) )); - $browser = new Browser($this->loop, $connector); + $browser = new Browser($connector, $this->loop); Block\await($browser->get('https://self-signed.badssl.com/'), $this->loop); } From 30c33fe4673357cb083e65965ab7bf7de1e59cd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 27 Jul 2021 17:34:50 +0200 Subject: [PATCH 067/152] Simplify usage by supporting new Socket API without nullable loop args --- README.md | 31 ++--- composer.json | 2 +- examples/11-client-http-connect-proxy.php | 2 +- examples/12-client-socks-proxy.php | 2 +- examples/13-client-ssh-proxy.php | 2 +- examples/51-server-hello-world.php | 2 +- examples/52-server-count-visitors.php | 2 +- examples/53-server-whatsmyip.php | 2 +- examples/54-server-query-parameter.php | 2 +- examples/55-server-cookie-handling.php | 2 +- examples/56-server-sleep.php | 2 +- examples/57-server-error-handling.php | 2 +- examples/58-server-stream-response.php | 2 +- examples/59-server-json-api.php | 2 +- examples/61-server-hello-world-https.php | 10 +- examples/62-server-form-upload.php | 2 +- examples/63-server-streaming-request.php | 2 +- examples/71-server-http-proxy.php | 2 +- examples/72-server-http-connect-proxy.php | 2 +- examples/81-server-upgrade-echo.php | 2 +- examples/82-server-upgrade-chat.php | 2 +- examples/99-server-benchmark-download.php | 2 +- src/Browser.php | 2 +- src/Client/Client.php | 2 +- src/HttpServer.php | 20 +-- src/Io/Sender.php | 2 +- src/Io/StreamingServer.php | 6 +- tests/Client/FunctionalIntegrationTest.php | 20 +-- tests/FunctionalBrowserTest.php | 21 ++-- tests/FunctionalHttpServerTest.php | 136 ++++++++++----------- 30 files changed, 146 insertions(+), 144 deletions(-) diff --git a/README.md b/README.md index 0cfd0307..49d8ecbc 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf ); }); -$socket = new React\Socket\Server(8080); +$socket = new React\Socket\SocketServer('127.0.0.1:8080'); $http->listen($socket); ``` @@ -260,7 +260,6 @@ like this: ```php $browser = new React\Http\Browser( new React\Socket\Connector( - null, array( 'timeout' => 5 ) @@ -604,7 +603,7 @@ $proxy = new Clue\React\HttpProxy\ProxyConnector( new React\Socket\Connector() ); -$connector = new React\Socket\Connector(null, array( +$connector = new React\Socket\Connector(array( 'tcp' => $proxy, 'dns' => false )); @@ -631,7 +630,7 @@ $proxy = new Clue\React\Socks\Client( new React\Socket\Connector() ); -$connector = new React\Socket\Connector(null, array( +$connector = new React\Socket\Connector(array( 'tcp' => $proxy, 'dns' => false )); @@ -660,7 +659,7 @@ plain HTTP and TLS-encrypted HTTPS. ```php $proxy = new Clue\React\SshProxy\SshSocksConnector('me@localhost:22', Loop::get()); -$connector = new React\Socket\Connector(null, array( +$connector = new React\Socket\Connector(array( 'tcp' => $proxy, 'dns' => false )); @@ -741,13 +740,13 @@ to be attached to an instance of [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) through the [`listen()`](#listen) method as described in the following chapter. In its most simple form, you can attach this to a -[`React\Socket\Server`](https://github.com/reactphp/socket#server) in order -to start a plaintext HTTP server like this: +[`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) +in order to start a plaintext HTTP server like this: ```php $http = new React\Http\HttpServer($handler); -$socket = new React\Socket\Server('0.0.0.0:8080'); +$socket = new React\Socket\SocketServer('0.0.0.0:8080'); $http->listen($socket); ``` @@ -869,13 +868,13 @@ is responsible for emitting the underlying streaming connections. This HTTP server needs to be attached to it in order to process any connections and pase incoming streaming data as incoming HTTP request messages. In its most common form, you can attach this to a -[`React\Socket\Server`](https://github.com/reactphp/socket#server) in -order to start a plaintext HTTP server like this: +[`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) +in order to start a plaintext HTTP server like this: ```php $http = new React\Http\HttpServer($handler); -$socket = new React\Socket\Server('0.0.0.0:8080'); +$socket = new React\Socket\SocketServer('0.0.0.0:8080'); $http->listen($socket); ``` @@ -894,15 +893,17 @@ Likewise, it's usually recommended to use a reverse proxy setup to accept secure HTTPS requests on default HTTPS port `443` (TLS termination) and only route plaintext requests to this HTTP server. As an alternative, you can also accept secure HTTPS requests with this HTTP server by attaching -this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) +this to a [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) using a secure TLS listen address, a certificate file and optional `passphrase` like this: ```php $http = new React\Http\HttpServer($handler); -$socket = new React\Socket\Server('tls://0.0.0.0:8443', null, array( - 'local_cert' => __DIR__ . '/localhost.pem' +$socket = new React\Socket\SocketServer('tls://0.0.0.0:8443', array( + 'tls' => array( + 'local_cert' => __DIR__ . '/localhost.pem' + ) )); $http->listen($socket); ``` @@ -1889,7 +1890,7 @@ 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(null, array( +$connector = new React\Socket\Connector(array( 'dns' => '127.0.0.1', 'tcp' => array( 'bindto' => '192.168.10.1:0' diff --git a/composer.json b/composer.json index 6673b232..44387e53 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "react/event-loop": "^1.2", "react/promise": "^2.3 || ^1.2.1", "react/promise-stream": "^1.1", - "react/socket": "^1.8", + "react/socket": "^1.9", "react/stream": "^1.2", "ringcentral/psr7": "^1.2" }, diff --git a/examples/11-client-http-connect-proxy.php b/examples/11-client-http-connect-proxy.php index ca7104aa..39b0cbcb 100644 --- a/examples/11-client-http-connect-proxy.php +++ b/examples/11-client-http-connect-proxy.php @@ -17,7 +17,7 @@ $proxy = new HttpConnectClient('127.0.0.1:8080', new Connector()); // create a Browser object that uses the HTTP CONNECT proxy client for connections -$connector = new Connector(null, array( +$connector = new Connector(array( 'tcp' => $proxy, 'dns' => false )); diff --git a/examples/12-client-socks-proxy.php b/examples/12-client-socks-proxy.php index 5801b138..ce020ad8 100644 --- a/examples/12-client-socks-proxy.php +++ b/examples/12-client-socks-proxy.php @@ -14,7 +14,7 @@ $proxy = new SocksClient('127.0.0.1:1080', new Connector()); // create a Browser object that uses the SOCKS proxy client for connections -$connector = new Connector(null, array( +$connector = new Connector(array( 'tcp' => $proxy, 'dns' => false )); diff --git a/examples/13-client-ssh-proxy.php b/examples/13-client-ssh-proxy.php index 764fb30f..d4acaba0 100644 --- a/examples/13-client-ssh-proxy.php +++ b/examples/13-client-ssh-proxy.php @@ -13,7 +13,7 @@ $proxy = new SshSocksConnector(isset($argv[1]) ? $argv[1] : 'localhost:22', Loop::get()); // create a Browser object that uses the SSH proxy client for connections -$connector = new Connector(null, array( +$connector = new Connector(array( 'tcp' => $proxy, 'dns' => false )); diff --git a/examples/51-server-hello-world.php b/examples/51-server-hello-world.php index 50c82396..f549ece8 100644 --- a/examples/51-server-hello-world.php +++ b/examples/51-server-hello-world.php @@ -15,7 +15,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/52-server-count-visitors.php b/examples/52-server-count-visitors.php index 1884acb0..d52285d0 100644 --- a/examples/52-server-count-visitors.php +++ b/examples/52-server-count-visitors.php @@ -16,7 +16,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/53-server-whatsmyip.php b/examples/53-server-whatsmyip.php index 89dad25c..5df1050d 100644 --- a/examples/53-server-whatsmyip.php +++ b/examples/53-server-whatsmyip.php @@ -17,7 +17,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/54-server-query-parameter.php b/examples/54-server-query-parameter.php index 2ab16647..22be7566 100644 --- a/examples/54-server-query-parameter.php +++ b/examples/54-server-query-parameter.php @@ -24,7 +24,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index ff89fe1e..a6858061 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -30,7 +30,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/56-server-sleep.php b/examples/56-server-sleep.php index bd2ea694..caa22644 100644 --- a/examples/56-server-sleep.php +++ b/examples/56-server-sleep.php @@ -22,7 +22,7 @@ }); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/57-server-error-handling.php b/examples/57-server-error-handling.php index f5281c53..4a1b6757 100644 --- a/examples/57-server-error-handling.php +++ b/examples/57-server-error-handling.php @@ -27,7 +27,7 @@ }); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php index d965e306..2069b7a8 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -38,7 +38,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/59-server-json-api.php b/examples/59-server-json-api.php index c06a9702..7fa8cc66 100644 --- a/examples/59-server-json-api.php +++ b/examples/59-server-json-api.php @@ -52,7 +52,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index 34f1a8bd..e5e0ed84 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -15,12 +15,14 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$socket = new React\Socket\SecureServer($socket, null, array( - 'local_cert' => isset($argv[2]) ? $argv[2] : __DIR__ . '/localhost.pem' +$uri = 'tls://' . (isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($uri, array( + 'tls' => array( + 'local_cert' => isset($argv[2]) ? $argv[2] : __DIR__ . '/localhost.pem' + ) )); $http->listen($socket); -//$socket->on('error', 'printf'); +$socket->on('error', 'printf'); echo 'Listening on ' . str_replace('tls:', 'https:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index 5a0fdace..6984b4e3 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -128,7 +128,7 @@ $handler ); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index 693cf2c0..2b5f8a6c 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -44,7 +44,7 @@ function (Psr\Http\Message\ServerRequestInterface $request) { $http->on('error', 'printf'); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index f6ab5ff5..c4fe244e 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -44,7 +44,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/72-server-http-connect-proxy.php b/examples/72-server-http-connect-proxy.php index 4399614b..205dd1da 100644 --- a/examples/72-server-http-connect-proxy.php +++ b/examples/72-server-http-connect-proxy.php @@ -50,7 +50,7 @@ function ($e) { ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/81-server-upgrade-echo.php b/examples/81-server-upgrade-echo.php index cb5d0b0d..2f77172f 100644 --- a/examples/81-server-upgrade-echo.php +++ b/examples/81-server-upgrade-echo.php @@ -56,7 +56,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/82-server-upgrade-chat.php b/examples/82-server-upgrade-chat.php index 22fed927..42635e8c 100644 --- a/examples/82-server-upgrade-chat.php +++ b/examples/82-server-upgrade-chat.php @@ -84,7 +84,7 @@ ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index bdc6735a..df0e69e7 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -122,7 +122,7 @@ public function getSize() ); }); -$socket = new React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/src/Browser.php b/src/Browser.php index 11519094..5879b977 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -47,7 +47,7 @@ class Browser * [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): * * ```php - * $connector = new React\Socket\Connector(null, array( + * $connector = new React\Socket\Connector(array( * 'dns' => '127.0.0.1', * 'tcp' => array( * 'bindto' => '192.168.10.1:0' diff --git a/src/Client/Client.php b/src/Client/Client.php index f28ec289..7a97349c 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -16,7 +16,7 @@ class Client public function __construct(LoopInterface $loop, ConnectorInterface $connector = null) { if ($connector === null) { - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); } $this->connector = $connector; diff --git a/src/HttpServer.php b/src/HttpServer.php index 582ea380..d5f947d5 100644 --- a/src/HttpServer.php +++ b/src/HttpServer.php @@ -54,13 +54,13 @@ * [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) * through the [`listen()`](#listen) method as described in the following * chapter. In its most simple form, you can attach this to a - * [`React\Socket\Server`](https://github.com/reactphp/socket#server) in order - * to start a plaintext HTTP server like this: + * [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) + * in order to start a plaintext HTTP server like this: * * ```php * $http = new React\Http\HttpServer($handler); * - * $socket = new React\Socket\Server('0.0.0.0:8080'); + * $socket = new React\Socket\SocketServer('0.0.0.0:8080'); * $http->listen($socket); * ``` * @@ -263,13 +263,13 @@ public function __construct($requestHandlerOrLoop) * HTTP server needs to be attached to it in order to process any * connections and pase incoming streaming data as incoming HTTP request * messages. In its most common form, you can attach this to a - * [`React\Socket\Server`](https://github.com/reactphp/socket#server) in - * order to start a plaintext HTTP server like this: + * [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) + * in order to start a plaintext HTTP server like this: * * ```php * $http = new React\Http\HttpServer($handler); * - * $socket = new React\Socket\Server(8080); + * $socket = new React\Socket\SocketServer('0.0.0.0:8080'); * $http->listen($socket); * ``` * @@ -288,15 +288,17 @@ public function __construct($requestHandlerOrLoop) * secure HTTPS requests on default HTTPS port `443` (TLS termination) and * only route plaintext requests to this HTTP server. As an alternative, you * can also accept secure HTTPS requests with this HTTP server by attaching - * this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) + * this to a [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) * using a secure TLS listen address, a certificate file and optional * `passphrase` like this: * * ```php * $http = new React\Http\HttpServer($handler); * - * $socket = new React\Socket\Server('tls://0.0.0.0:8443', null, array( - * 'local_cert' => __DIR__ . '/localhost.pem' + * $socket = new React\Socket\SocketServer('tls://0.0.0.0:8443', array( + * 'tls' => array( + * 'local_cert' => __DIR__ . '/localhost.pem' + * ) * )); * $http->listen($socket); * ``` diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 1eb098c6..c1bbab42 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -39,7 +39,7 @@ class Sender * settings. You can use this method manually like this: * * ```php - * $connector = new \React\Socket\Connector($loop); + * $connector = new \React\Socket\Connector(array(), $loop); * $sender = \React\Http\Io\Sender::createFromLoop($loop, $connector); * ``` * diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 95e0e4c6..2c912dfa 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -20,7 +20,7 @@ * The internal `StreamingServer` class is responsible for handling incoming connections and then * processing each incoming HTTP request. * - * Unlike the [`HttpServer`](#server) class, it does not buffer and parse the incoming + * Unlike the [`HttpServer`](#httpserver) class, it does not buffer and parse the incoming * HTTP request body by default. This means that the request handler will be * invoked with a streaming request body. Once the request headers have been * received, it will invoke the request handler function. This request handler @@ -50,13 +50,13 @@ * In order to process any connections, the server needs to be attached to an * instance of `React\Socket\ServerInterface` through the [`listen()`](#listen) method * as described in the following chapter. In its most simple form, you can attach - * this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server) + * this to a [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) * in order to start a plaintext HTTP server like this: * * ```php * $server = new StreamingServer($loop, $handler); * - * $socket = new React\Socket\Server('0.0.0.0:8080', $loop); + * $socket = new React\Socket\SocketServer('0.0.0.0:8080', array(), $loop); * $server->listen($socket); * ``` * diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 3e07803a..6a62be93 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -8,8 +8,8 @@ use React\Http\Client\Client; use React\Promise\Deferred; use React\Promise\Stream; -use React\Socket\Server; use React\Socket\ConnectionInterface; +use React\Socket\SocketServer; use React\Stream\ReadableStreamInterface; use React\Tests\Http\TestCase; @@ -39,13 +39,13 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() { $loop = Factory::create(); - $server = new Server(0, $loop); - $server->on('connection', $this->expectCallableOnce()); - $server->on('connection', function (ConnectionInterface $conn) use ($server) { + $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket->on('connection', $this->expectCallableOnce()); + $socket->on('connection', function (ConnectionInterface $conn) use ($socket) { $conn->end("HTTP/1.1 200 OK\r\n\r\nOk"); - $server->close(); + $socket->close(); }); - $port = parse_url(https://github.com/$server-%3EgetAddress(), PHP_URL_PORT); + $port = parse_url(https://github.com/$socket-%3EgetAddress(), PHP_URL_PORT); $client = new Client($loop); $request = $client->request('GET', 'http://localhost/' . $port); @@ -60,14 +60,14 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp { $loop = Factory::create(); - $server = new Server(0, $loop); - $server->on('connection', function (ConnectionInterface $conn) use ($server) { + $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket->on('connection', function (ConnectionInterface $conn) use ($socket) { $conn->end("HTTP/1.0 200 OK\n\nbody"); - $server->close(); + $socket->close(); }); $client = new Client($loop); - $request = $client->request('GET', str_replace('tcp:', 'http:', $server->getAddress())); + $request = $client->request('GET', str_replace('tcp:', 'http:', $socket->getAddress())); $once = $this->expectCallableOnceWith('body'); $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) { diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index a46e74a6..5db62003 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -14,6 +14,7 @@ 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; @@ -141,7 +142,7 @@ public function setUpBrowserAndServer() var_dump($path); }); - $socket = new \React\Socket\Server(0, $this->loop); + $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; @@ -392,11 +393,11 @@ public function testVerifyPeerEnabledForBadSslRejects() $this->markTestSkipped('Not supported on HHVM'); } - $connector = new Connector($this->loop, array( + $connector = new Connector(array( 'tls' => array( 'verify_peer' => true ) - )); + ), $this->loop); $browser = new Browser($connector, $this->loop); @@ -414,11 +415,11 @@ public function testVerifyPeerDisabledForBadSslResolves() $this->markTestSkipped('Not supported on HHVM'); } - $connector = new Connector($this->loop, array( + $connector = new Connector(array( 'tls' => array( 'verify_peer' => false ) - )); + ), $this->loop); $browser = new Browser($connector, $this->loop); @@ -497,7 +498,7 @@ public function testRequestStreamWithHeadRequestReturnsEmptyResponseBodWithTrans public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenResponseHasDoubleTransferEncoding() { - $socket = new \React\Socket\Server(0, $this->loop); + $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) { $connection->on('data', function () use ($connection) { $connection->end("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked, chunked\r\nConnection: close\r\n\r\nhello"); @@ -517,7 +518,7 @@ public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenRes public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeepsConnectionOpen() { $closed = new \React\Promise\Deferred(); - $socket = new \React\Socket\Server(0, $this->loop); + $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) use ($closed) { $connection->on('data', function () use ($connection) { $connection->write("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"); @@ -576,7 +577,7 @@ public function testPostStreamWillStartSendingRequestEvenWhenBodyDoesNotEmitData $http = new HttpServer($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { return new Response(200); }); - $socket = new \React\Socket\Server(0, $this->loop); + $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; @@ -607,7 +608,7 @@ public function testSendsHttp11ByDefault() $request->getProtocolVersion() ); }); - $socket = new \React\Socket\Server(0, $this->loop); + $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; @@ -627,7 +628,7 @@ public function testSendsExplicitHttp10Request() $request->getProtocolVersion() ); }); - $socket = new \React\Socket\Server(0, $this->loop); + $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; diff --git a/tests/FunctionalHttpServerTest.php b/tests/FunctionalHttpServerTest.php index f7aa8392..fe0e1936 100644 --- a/tests/FunctionalHttpServerTest.php +++ b/tests/FunctionalHttpServerTest.php @@ -11,10 +11,9 @@ use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\StreamingRequestMiddleware; -use React\Socket\Server as Socket; -use React\Socket\Connector; use React\Socket\ConnectionInterface; -use React\Socket\SecureServer; +use React\Socket\Connector; +use React\Socket\SocketServer; use React\Promise; use React\Promise\Stream; use React\Stream\ThroughStream; @@ -24,13 +23,13 @@ class FunctionalHttpServerTest extends TestCase public function testPlainHttpOnRandomPort() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -50,7 +49,7 @@ public function testPlainHttpOnRandomPort() public function testPlainHttpOnRandomPortWithSingleRequestHandlerArray() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer( $loop, @@ -59,7 +58,7 @@ function () { } ); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -78,13 +77,13 @@ function () { public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -104,13 +103,13 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -134,18 +133,17 @@ public function testSecureHttpsOnRandomPort() } $loop = Factory::create(); - $connector = new Connector($loop, array( + $connector = new Connector(array( 'tls' => array('verify_peer' => false) - )); + ), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $socket = new Socket(0, $loop); - $socket = new SecureServer($socket, $loop, array( + $socket = new SocketServer('tls://127.0.0.1:0', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )); + )), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -178,15 +176,14 @@ public function testSecureHttpsReturnsData() ); }); - $socket = new Socket(0, $loop); - $socket = new SecureServer($socket, $loop, array( + $socket = new SocketServer('tls://127.0.0.1:0', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )); + )), $loop); $http->listen($socket); - $connector = new Connector($loop, array( + $connector = new Connector(array( 'tls' => array('verify_peer' => false) - )); + ), $loop); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); @@ -210,18 +207,17 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() } $loop = Factory::create(); - $connector = new Connector($loop, array( + $connector = new Connector(array( 'tls' => array('verify_peer' => false) - )); + ), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $socket = new Socket(0, $loop); - $socket = new SecureServer($socket, $loop, array( + $socket = new SocketServer('tls://127.0.0.1:0', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )); + )), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -242,11 +238,11 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() { $loop = Factory::create(); try { - $socket = new Socket(80, $loop); + $socket = new SocketServer('127.0.0.1:80', array(), $loop); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 80 failed (root and unused?)'); } - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); @@ -272,11 +268,11 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort { $loop = Factory::create(); try { - $socket = new Socket(80, $loop); + $socket = new SocketServer('127.0.0.1:80', array(), $loop); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 80 failed (root and unused?)'); } - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); @@ -306,16 +302,16 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() $loop = Factory::create(); try { - $socket = new Socket(443, $loop); + $socket = new SocketServer('127.0.0.1:443', array('tls' => array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )), $loop); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 443 failed (root and unused?)'); } - $socket = new SecureServer($socket, $loop, array( - 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )); - $connector = new Connector($loop, array( + + $connector = new Connector(array( 'tls' => array('verify_peer' => false) - )); + ), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); @@ -345,16 +341,16 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() $loop = Factory::create(); try { - $socket = new Socket(443, $loop); + $socket = new SocketServer('127.0.0.1:443', array('tls' => array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )), $loop); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 443 failed (root and unused?)'); } - $socket = new SecureServer($socket, $loop, array( - 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )); - $connector = new Connector($loop, array( + + $connector = new Connector(array( 'tls' => array('verify_peer' => false) - )); + ), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); @@ -380,11 +376,11 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() { $loop = Factory::create(); try { - $socket = new Socket(443, $loop); + $socket = new SocketServer('127.0.0.1:443', array(), $loop); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 443 failed (root and unused?)'); } - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); @@ -414,16 +410,16 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() $loop = Factory::create(); try { - $socket = new Socket(80, $loop); + $socket = new SocketServer('127.0.0.1:80', array('tls' => array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )), $loop); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 80 failed (root and unused?)'); } - $socket = new SecureServer($socket, $loop, array( - 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )); - $connector = new Connector($loop, array( + + $connector = new Connector(array( 'tls' => array('verify_peer' => false) - )); + ), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri() . 'x' . $request->getHeaderLine('Host')); @@ -448,7 +444,7 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() public function testClosedStreamFromRequestHandlerWillSendEmptyBody() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $stream = new ThroughStream(); $stream->close(); @@ -457,7 +453,7 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() return new Response(200, array(), $stream); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -477,7 +473,7 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() public function testRequestHandlerWithStreamingRequestWillReceiveCloseEventIfConnectionClosesWhileSendingBody() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $once = $this->expectCallableOnce(); $http = new HttpServer( @@ -488,7 +484,7 @@ function (RequestInterface $request) use ($once) { } ); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { @@ -507,7 +503,7 @@ function (RequestInterface $request) use ($once) { public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileSendingStreamingRequestBody() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $stream = new ThroughStream(); @@ -519,7 +515,7 @@ function (RequestInterface $request) use ($stream) { } ); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { @@ -541,7 +537,7 @@ function (RequestInterface $request) use ($stream) { public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $stream = new ThroughStream(); @@ -549,7 +545,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() return new Response(200, array(), $stream); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { @@ -571,7 +567,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() public function testUpgradeWithThroughStreamReturnsDataAsGiven() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); @@ -583,7 +579,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() return new Response(101, array('Upgrade' => 'echo'), $stream); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -608,7 +604,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); @@ -620,7 +616,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() return new Response(101, array('Upgrade' => 'echo'), $stream); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -646,7 +642,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() public function testConnectWithThroughStreamReturnsDataAsGiven() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); @@ -658,7 +654,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() return new Response(200, array(), $stream); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -683,7 +679,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGiven() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { $stream = new ThroughStream(); @@ -699,7 +695,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive }); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -724,7 +720,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive public function testConnectWithClosedThroughStreamReturnsNoData() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer($loop, function (RequestInterface $request) { $stream = new ThroughStream(); @@ -733,7 +729,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() return new Response(200, array(), $stream); }); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -758,7 +754,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing() { $loop = Factory::create(); - $connector = new Connector($loop); + $connector = new Connector(array(), $loop); $http = new HttpServer( $loop, @@ -776,7 +772,7 @@ function (ServerRequestInterface $request) { } ); - $socket = new Socket(0, $loop); + $socket = new SocketServer('127.0.0.1:0', array(), $loop); $http->listen($socket); $result = array(); From 8a0fd7c0aa74f0db3008b1e47ca86c613cbb040e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 4 Aug 2021 14:24:55 +0200 Subject: [PATCH 068/152] Prepare v1.5.0 release --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ README.md | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f562f66d..e12b787e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # Changelog +## 1.5.0 (2021-08-04) + +* Feature: Update `Browser` signature to take optional `$connector` as first argument and + to match new Socket API without nullable loop arguments. + (#418 and #419 by @clue) + + ```php + // unchanged + $browser = new React\Http\Browser(); + + // deprecated + $browser = new React\Http\Browser(null, $connector); + $browser = new React\Http\Browser($loop, $connector); + + // new + $browser = new React\Http\Browser($connector); + $browser = new React\Http\Browser($connector, $loop); + ``` + +* Feature: Rename `Server` to `HttpServer` to avoid class name collisions and + to avoid any ambiguities with regards to the new `SocketServer` API. + (#417 and #419 by @clue) + + ```php + // deprecated + $server = new React\Http\Server($handler); + $server->listen(new React\Socket\Server(8080)); + + // new + $http = new React\Http\HttpServer($handler); + $http->listen(new React\Socket\SocketServer('127.0.0.1:8080')); + ``` + ## 1.4.0 (2021-07-11) A major new feature release, see [**release announcement**](https://clue.engineering/2021/announcing-reactphp-default-loop). diff --git a/README.md b/README.md index 49d8ecbc..63865d5c 100644 --- a/README.md +++ b/README.md @@ -2738,7 +2738,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require react/http:^1.3 +$ composer require react/http:^1.5 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 40ecb8f6dfa5c424a291c5784a1f6086913e1a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 25 Jul 2021 21:27:57 +0200 Subject: [PATCH 069/152] Update proxy examples to use simplified APIs thanks to default loop --- README.md | 14 ++++---------- composer.json | 6 +++--- ...-connect-proxy.php => 11-client-http-proxy.php} | 9 ++++----- examples/12-client-socks-proxy.php | 10 ++++++---- examples/13-client-ssh-proxy.php | 9 ++++----- 5 files changed, 21 insertions(+), 27 deletions(-) rename examples/{11-client-http-connect-proxy.php => 11-client-http-proxy.php} (70%) diff --git a/README.md b/README.md index 63865d5c..5d44199c 100644 --- a/README.md +++ b/README.md @@ -598,10 +598,7 @@ to HTTPS port`443` only, this can technically be used to tunnel any TCP/IP-based protocol, such as plain HTTP and TLS-encrypted HTTPS. ```php -$proxy = new Clue\React\HttpProxy\ProxyConnector( - 'http://127.0.0.1:8080/', - new React\Socket\Connector() -); +$proxy = new Clue\React\HttpProxy\ProxyConnector('127.0.0.1:8080'); $connector = new React\Socket\Connector(array( 'tcp' => $proxy, @@ -611,7 +608,7 @@ $connector = new React\Socket\Connector(array( $browser = new React\Http\Browser($connector); ``` -See also the [HTTP CONNECT proxy example](examples/11-client-http-connect-proxy.php). +See also the [HTTP proxy example](examples/11-client-http-proxy.php). ### SOCKS proxy @@ -625,10 +622,7 @@ address (anonymity) or to circumvent address blocking (geoblocking). While many only, this can technically be used to tunnel any TCP/IP-based protocol. ```php -$proxy = new Clue\React\Socks\Client( - 'socks://127.0.0.1:1080', - new React\Socket\Connector() -); +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); $connector = new React\Socket\Connector(array( 'tcp' => $proxy, @@ -657,7 +651,7 @@ from the outside (database behind firewall) and as such can also be used for plain HTTP and TLS-encrypted HTTPS. ```php -$proxy = new Clue\React\SshProxy\SshSocksConnector('me@localhost:22', Loop::get()); +$proxy = new Clue\React\SshProxy\SshSocksConnector('alice@example.com'); $connector = new React\Socket\Connector(array( 'tcp' => $proxy, diff --git a/composer.json b/composer.json index 44387e53..25f96db5 100644 --- a/composer.json +++ b/composer.json @@ -38,9 +38,9 @@ }, "require-dev": { "clue/block-react": "^1.1", - "clue/http-proxy-react": "^1.3", - "clue/reactphp-ssh-proxy": "^1.0", - "clue/socks-react": "^1.0", + "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" }, "autoload": { diff --git a/examples/11-client-http-connect-proxy.php b/examples/11-client-http-proxy.php similarity index 70% rename from examples/11-client-http-connect-proxy.php rename to examples/11-client-http-proxy.php index 39b0cbcb..127ee477 100644 --- a/examples/11-client-http-connect-proxy.php +++ b/examples/11-client-http-proxy.php @@ -3,18 +3,17 @@ // not already running a HTTP CONNECT proxy server? // Try LeProxy.org or this: // -// $ php examples/72-server-http-connect-proxy.php 8080 -// $ php examples/11-client-http-connect-proxy.php +// $ php examples/72-server-http-connect-proxy.php 127.0.0.1:8080 +// $ http_proxy=127.0.0.1:8080 php examples/11-client-http-connect-proxy.php -use Clue\React\HttpProxy\ProxyConnector as HttpConnectClient; use Psr\Http\Message\ResponseInterface; use React\Http\Browser; use React\Socket\Connector; require __DIR__ . '/../vendor/autoload.php'; -// create a new HTTP CONNECT proxy client which connects to a HTTP CONNECT proxy server listening on localhost:8080 -$proxy = new HttpConnectClient('127.0.0.1:8080', new Connector()); +// create a new HTTP CONNECT proxy client which connects to a HTTP CONNECT proxy server listening on 127.0.0.1:8080 +$proxy = new Clue\React\HttpProxy\ProxyConnector(getenv('http_proxy') ?: '127.0.0.1:8080'); // create a Browser object that uses the HTTP CONNECT proxy client for connections $connector = new Connector(array( diff --git a/examples/12-client-socks-proxy.php b/examples/12-client-socks-proxy.php index ce020ad8..c3e662a9 100644 --- a/examples/12-client-socks-proxy.php +++ b/examples/12-client-socks-proxy.php @@ -1,17 +1,19 @@ Date: Tue, 17 Aug 2021 20:35:10 +0200 Subject: [PATCH 070/152] Don't run requests through configuration middleware While working on another PR that introduces another configuration middleware it dawned on me that we preferably don't requests through middleware that don't do anything. This change will filter it out those middleware before they are passed into the MiddlewareRunner. While running benchmarks I gained around a 10% requests per second gain when running it against example 63. --- src/HttpServer.php | 8 ++++++++ tests/HttpServerTest.php | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/HttpServer.php b/src/HttpServer.php index d5f947d5..95ccc5ff 100644 --- a/src/HttpServer.php +++ b/src/HttpServer.php @@ -247,6 +247,14 @@ public function __construct($requestHandlerOrLoop) $middleware = \array_merge($middleware, $requestHandlers); + /** + * Filter out any configuration middleware, no need to run requests through something that isn't + * doing anything with the request. + */ + $middleware = \array_filter($middleware, function ($handler) { + return !($handler instanceof StreamingRequestMiddleware); + }); + $this->streamingServer = new StreamingServer($loop, new MiddlewareRunner($middleware)); $that = $this; diff --git a/tests/HttpServerTest.php b/tests/HttpServerTest.php index 9a0a789a..9200aa66 100644 --- a/tests/HttpServerTest.php +++ b/tests/HttpServerTest.php @@ -431,4 +431,27 @@ public function testConstructServerWithMemoryLimitDoesLimitConcurrency() $this->assertTrue(is_array($middleware)); $this->assertInstanceOf('React\Http\Middleware\LimitConcurrentRequestsMiddleware', $middleware[0]); } + + public function testConstructFiltersOutConfigurationMiddlewareBefore() + { + $http = new HttpServer(new StreamingRequestMiddleware(), function () { }); + + $ref = new \ReflectionProperty($http, 'streamingServer'); + $ref->setAccessible(true); + + $streamingServer = $ref->getValue($http); + + $ref = new \ReflectionProperty($streamingServer, 'callback'); + $ref->setAccessible(true); + + $middlewareRunner = $ref->getValue($streamingServer); + + $ref = new \ReflectionProperty($middlewareRunner, 'middleware'); + $ref->setAccessible(true); + + $middleware = $ref->getValue($middlewareRunner); + + $this->assertTrue(is_array($middleware)); + $this->assertCount(1, $middleware); + } } From bf32f168b28a12e5666928ee884d591939e448d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 21 Aug 2021 13:39:53 +0200 Subject: [PATCH 071/152] Fix docs for missing `$body` argument for `put()` and `delete()` methods --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5d44199c..7bc96076 100644 --- a/README.md +++ b/README.md @@ -2015,7 +2015,7 @@ $browser->patch($url, array('Content-Length' => '11'), $body); #### put() -The `put(string $url, array $headers = array()): PromiseInterface` method can be used to +The `put(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an HTTP PUT request. ```php @@ -2049,7 +2049,7 @@ $browser->put($url, array('Content-Length' => '11'), $body); #### delete() -The `delete(string $url, array $headers = array()): PromiseInterface` method can be used to +The `delete(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an HTTP DELETE request. ```php From 6785514d6cef6304822bd6aeac837cd97c5e8cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 25 Aug 2021 16:40:34 +0200 Subject: [PATCH 072/152] Consistently use `$body` argument name for request body --- README.md | 12 ++++++------ src/Browser.php | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 7bc96076..ba7c5cf9 100644 --- a/README.md +++ b/README.md @@ -123,10 +123,10 @@ offers several methods that resemble the HTTP protocol methods: ```php $browser->get($url, array $headers = array()); $browser->head($url, array $headers = array()); -$browser->post($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); -$browser->delete($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); -$browser->put($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); -$browser->patch($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); +$browser->post($url, array $headers = array(), string|ReadableStreamInterface $body = ''); +$browser->delete($url, array $headers = array(), string|ReadableStreamInterface $body = ''); +$browser->put($url, array $headers = array(), string|ReadableStreamInterface $body = ''); +$browser->patch($url, array $headers = array(), string|ReadableStreamInterface $body = ''); ``` Each of these methods requires a `$url` and some optional parameters to send an @@ -1921,7 +1921,7 @@ See also [GET request client example](examples/01-client-get-request.php). #### post() -The `post(string $url, array $headers = array(), string|ReadableStreamInterface $contents = ''): PromiseInterface` method can be used to +The `post(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an HTTP POST request. ```php @@ -1983,7 +1983,7 @@ $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $respons #### patch() -The `patch(string $url, array $headers = array(), string|ReadableStreamInterface $contents = ''): PromiseInterface` method can be used to +The `patch(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an HTTP PATCH request. ```php diff --git a/src/Browser.php b/src/Browser.php index 5879b977..657b43b0 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -162,12 +162,12 @@ public function get($url, array $headers = array()) * * @param string $url URL for the request. * @param array $headers - * @param string|ReadableStreamInterface $contents + * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - public function post($url, array $headers = array(), $contents = '') + public function post($url, array $headers = array(), $body = '') { - return $this->requestMayBeStreaming('POST', $url, $headers, $contents); + return $this->requestMayBeStreaming('POST', $url, $headers, $body); } /** @@ -220,12 +220,12 @@ public function head($url, array $headers = array()) * * @param string $url URL for the request. * @param array $headers - * @param string|ReadableStreamInterface $contents + * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - public function patch($url, array $headers = array(), $contents = '') + public function patch($url, array $headers = array(), $body = '') { - return $this->requestMayBeStreaming('PATCH', $url , $headers, $contents); + return $this->requestMayBeStreaming('PATCH', $url , $headers, $body); } /** @@ -262,12 +262,12 @@ public function patch($url, array $headers = array(), $contents = '') * * @param string $url URL for the request. * @param array $headers - * @param string|ReadableStreamInterface $contents + * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - public function put($url, array $headers = array(), $contents = '') + public function put($url, array $headers = array(), $body = '') { - return $this->requestMayBeStreaming('PUT', $url, $headers, $contents); + return $this->requestMayBeStreaming('PUT', $url, $headers, $body); } /** @@ -281,12 +281,12 @@ public function put($url, array $headers = array(), $contents = '') * * @param string $url URL for the request. * @param array $headers - * @param string|ReadableStreamInterface $contents + * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - public function delete($url, array $headers = array(), $contents = '') + public function delete($url, array $headers = array(), $body = '') { - return $this->requestMayBeStreaming('DELETE', $url, $headers, $contents); + return $this->requestMayBeStreaming('DELETE', $url, $headers, $body); } /** From f0e7249ccf3c6186eb1044f9e78247ce11f564c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 Sep 2021 13:38:38 +0200 Subject: [PATCH 073/152] Minor documentation improvements --- README.md | 18 +++++++++++++----- examples/11-client-http-proxy.php | 4 ++-- examples/72-server-http-connect-proxy.php | 2 +- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ba7c5cf9..0f3919db 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,14 @@ multiple concurrent HTTP requests without blocking. ## Quickstart example -Once [installed](#install), you can use the following code to access a -HTTP webserver and send some simple HTTP GET requests: +Once [installed](#install), you can use the following code to access an +HTTP web server and send some simple HTTP GET requests: ```php +get('http://www.google.com/')->then(function (Psr\Http\Message\ResponseInterface $response) { @@ -97,6 +101,10 @@ $client->get('http://www.google.com/')->then(function (Psr\Http\Message\Response This is an HTTP server which responds with `Hello World!` to every request. ```php + 'text/plain', 'Allow' => 'CONNECT' ), - 'This is a HTTP CONNECT (secure HTTPS) proxy' + 'This is an HTTP CONNECT (secure HTTPS) proxy' ); } From fb82de172af8a460083747ae1fa9253ebea3e68a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 27 Aug 2021 21:38:48 +0200 Subject: [PATCH 074/152] Improve error reporting in examples and documentation --- README.md | 63 ++++++++++++++++--- examples/01-client-get-request.php | 2 + examples/02-client-concurrent-requests.php | 6 ++ examples/03-client-request-any.php | 8 +++ examples/04-client-post-json.php | 4 +- examples/05-client-put-xml.php | 4 +- examples/11-client-http-proxy.php | 4 +- examples/12-client-socks-proxy.php | 4 +- examples/13-client-ssh-proxy.php | 4 +- examples/14-client-unix-domain-sockets.php | 4 +- .../21-client-request-streaming-to-stdout.php | 4 +- .../22-client-stream-upload-from-stdin.php | 4 +- examples/61-server-hello-world-https.php | 4 +- examples/63-server-streaming-request.php | 8 ++- examples/72-server-http-connect-proxy.php | 2 +- examples/91-client-benchmark-download.php | 4 +- examples/92-client-benchmark-upload.php | 2 +- src/Browser.php | 30 ++++++++- 18 files changed, 134 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index ba7c5cf9..b2348039 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,8 @@ $client = new React\Http\Browser(); $client->get('http://www.google.com/')->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -171,8 +173,8 @@ $browser->get($url)->then( function (Psr\Http\Message\ResponseInterface $response) { var_dump('Response received', $response); }, - function (Exception $error) { - var_dump('There was an error', $error->getMessage()); + function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; } ); ``` @@ -232,6 +234,8 @@ $browser = $browser->withTimeout(10.0); $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { // response received within 10 seconds maximum var_dump($response->getHeaders()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -319,6 +323,8 @@ The promise will be fulfilled with the last response from the chain of redirects $browser->get($url, $headers)->then(function (Psr\Http\Message\ResponseInterface $response) { // the final response will end up here var_dump($response->getHeaders()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -347,6 +353,8 @@ $browser = $browser->withFollowRedirects(false); $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { // any redirects will now end up here var_dump($response->getHeaders()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -410,6 +418,8 @@ from your side. foreach ($urls as $url) { $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump($response->getHeaders()); + }, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); } ``` @@ -429,6 +439,8 @@ $q = new Clue\React\Mq\Queue(10, null, function ($url) use ($browser) { foreach ($urls as $url) { $q($url)->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump($response->getHeaders()); + }, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); } ``` @@ -481,13 +493,15 @@ $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\Respons echo $chunk; }); - $body->on('error', function (Exception $error) { - echo 'Error: ' . $error->getMessage() . PHP_EOL; + $body->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); $body->on('close', function () { echo '[DONE]' . PHP_EOL; }); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -549,6 +563,9 @@ $stream = download($browser, $url); $stream->on('data', function ($data) { echo $data; }); +$stream->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); ``` See also the [`requestStreaming()`](#requeststreaming) method for more details. @@ -565,6 +582,8 @@ to the [request methods](#request-methods) like this: ```php $browser->post($url, array(), $stream)->then(function (Psr\Http\Message\ResponseInterface $response) { echo 'Successfully sent.'; +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -683,6 +702,8 @@ $browser = new React\Http\Browser($connector); $client->get('http://localhost/info')->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump($response->getHeaders(), (string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -1185,13 +1206,13 @@ $http = new React\Http\HttpServer( }); // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event - $body->on('error', function (\Exception $exception) use ($resolve, &$bytes) { + $body->on('error', function (Exception $e) use ($resolve, &$bytes) { $resolve(new React\Http\Message\Response( 400, array( 'Content-Type' => 'text/plain' ), - "Encountered error after $bytes bytes: {$exception->getMessage()}\n" + "Encountered error after $bytes bytes: {$e->getMessage()}\n" )); }); }); @@ -1914,6 +1935,8 @@ send an HTTP GET request. ```php $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump((string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -1933,6 +1956,8 @@ $browser->post( json_encode($data) )->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump(json_decode((string)$response->getBody())); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -1978,6 +2003,8 @@ send an HTTP HEAD request. ```php $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump($response->getHeaders()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -1995,6 +2022,8 @@ $browser->patch( json_encode($data) )->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump(json_decode((string)$response->getBody())); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -2027,6 +2056,8 @@ $browser->put( $xml->asXML() )->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump((string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -2055,6 +2086,8 @@ send an HTTP DELETE request. ```php $browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump((string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -2073,6 +2106,8 @@ can use this method: ```php $browser->request('OPTIONS', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { var_dump((string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -2124,13 +2159,15 @@ $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\Respons echo $chunk; }); - $body->on('error', function (Exception $error) { - echo 'Error: ' . $error->getMessage() . PHP_EOL; + $body->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); $body->on('close', function () { echo '[DONE]' . PHP_EOL; }); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -2209,6 +2246,8 @@ $browser = $browser->withFollowRedirects(0); $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { // only non-redirected responses will now end up here var_dump($response->getHeaders()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -2222,6 +2261,8 @@ $browser = $browser->withFollowRedirects(false); $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { // any redirects will now end up here var_dump($response->getHeaderLine('Location')); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -2253,6 +2294,8 @@ $browser = $browser->withRejectErrorResponse(false); $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { // any HTTP response will now end up here var_dump($response->getStatusCode(), $response->getReasonPhrase()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -2272,7 +2315,7 @@ $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response $response = $e->getResponse(); var_dump($response->getStatusCode(), $response->getReasonPhrase()); } else { - var_dump($e->getMessage()); + echo 'Error: ' . $e->getMessage() . PHP_EOL; } }); ``` @@ -2362,6 +2405,8 @@ $browser = $browser->withResponseBuffer(1024 * 1024); $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { // response body will not exceed 1 MiB var_dump($response->getHeaders(), (string) $response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` diff --git a/examples/01-client-get-request.php b/examples/01-client-get-request.php index 8e232398..34a79bbb 100644 --- a/examples/01-client-get-request.php +++ b/examples/01-client-get-request.php @@ -9,4 +9,6 @@ $client->get('http://google.com/')->then(function (ResponseInterface $response) { var_dump($response->getHeaders(), (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 dca1d9c1..7b1b77a0 100644 --- a/examples/02-client-concurrent-requests.php +++ b/examples/02-client-concurrent-requests.php @@ -9,12 +9,18 @@ $client->head('http://www.github.com/clue/http-react')->then(function (ResponseInterface $response) { var_dump($response->getHeaders(), (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()); +}, 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()); +}, 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 a3bd2831..0c96b684 100644 --- a/examples/03-client-request-any.php +++ b/examples/03-client-request-any.php @@ -26,4 +26,12 @@ var_dump($response->getHeaders()); echo PHP_EOL . $response->getBody(); +}, function ($e) { + // Promise v1 and v2 reject with an array of Exceptions here, Promise v3 will use an Exception object instead + if (is_array($e)) { + $e = end($e); + } + assert($e instanceof Exception); + + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); diff --git a/examples/04-client-post-json.php b/examples/04-client-post-json.php index 400b1a13..b01ada13 100644 --- a/examples/04-client-post-json.php +++ b/examples/04-client-post-json.php @@ -23,4 +23,6 @@ json_encode($data) )->then(function (ResponseInterface $response) { echo (string)$response->getBody(); -}, 'printf'); +}, 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 05804f23..231e2ca4 100644 --- a/examples/05-client-put-xml.php +++ b/examples/05-client-put-xml.php @@ -20,4 +20,6 @@ $xml->asXML() )->then(function (ResponseInterface $response) { echo (string)$response->getBody(); -}, 'printf'); +}, 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 127ee477..65eb8d0f 100644 --- a/examples/11-client-http-proxy.php +++ b/examples/11-client-http-proxy.php @@ -26,4 +26,6 @@ // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { echo RingCentral\Psr7\str($response); -}, 'printf'); +}, 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 c3e662a9..ecedf242 100644 --- a/examples/12-client-socks-proxy.php +++ b/examples/12-client-socks-proxy.php @@ -26,4 +26,6 @@ // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { echo RingCentral\Psr7\str($response); -}, 'printf'); +}, 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 54bb9208..64d0c282 100644 --- a/examples/13-client-ssh-proxy.php +++ b/examples/13-client-ssh-proxy.php @@ -22,4 +22,6 @@ // demo fetching HTTP headers (or bail out otherwise) $browser->get('https://www.google.com/')->then(function (ResponseInterface $response) { echo RingCentral\Psr7\str($response); -}, 'printf'); +}, 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 36bfecda..e9718141 100644 --- a/examples/14-client-unix-domain-sockets.php +++ b/examples/14-client-unix-domain-sockets.php @@ -19,4 +19,6 @@ // demo fetching HTTP headers (or bail out otherwise) $browser->get('http://localhost/info')->then(function (ResponseInterface $response) { echo Psr7\str($response); -}, 'printf'); +}, 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 3d2110a2..2f24d035 100644 --- a/examples/21-client-request-streaming-to-stdout.php +++ b/examples/21-client-request-streaming-to-stdout.php @@ -27,4 +27,6 @@ $body = $response->getBody(); assert($body instanceof ReadableStreamInterface); $body->pipe($out); -}, 'printf'); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); diff --git a/examples/22-client-stream-upload-from-stdin.php b/examples/22-client-stream-upload-from-stdin.php index a0857feb..b00fbc5e 100644 --- a/examples/22-client-stream-upload-from-stdin.php +++ b/examples/22-client-stream-upload-from-stdin.php @@ -21,4 +21,6 @@ $client->post($url, array(), $in)->then(function (ResponseInterface $response) { echo 'Received' . PHP_EOL . Psr7\str($response); -}, 'printf'); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index e5e0ed84..01182fdd 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -23,6 +23,8 @@ )); $http->listen($socket); -$socket->on('error', 'printf'); +$socket->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); echo 'Listening on ' . str_replace('tls:', 'https:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index 2b5f8a6c..c1e6ac89 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -29,20 +29,22 @@ function (Psr\Http\Message\ServerRequestInterface $request) { }); // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event - $body->on('error', function (\Exception $exception) use ($resolve, &$bytes) { + $body->on('error', function (Exception $e) use ($resolve, &$bytes) { $resolve(new React\Http\Message\Response( 400, array( 'Content-Type' => 'text/plain' ), - "Encountered error after $bytes bytes: {$exception->getMessage()}\n" + "Encountered error after $bytes bytes: {$e->getMessage()}\n" )); }); }); } ); -$http->on('error', 'printf'); +$http->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); $socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); $http->listen($socket); diff --git a/examples/72-server-http-connect-proxy.php b/examples/72-server-http-connect-proxy.php index 205dd1da..58143482 100644 --- a/examples/72-server-http-connect-proxy.php +++ b/examples/72-server-http-connect-proxy.php @@ -38,7 +38,7 @@ function (ConnectionInterface $remote) { $remote ); }, - function ($e) { + function (Exception $e) { return new Response( 502, array( diff --git a/examples/91-client-benchmark-download.php b/examples/91-client-benchmark-download.php index 2f76a3f3..49693baf 100644 --- a/examples/91-client-benchmark-download.php +++ b/examples/91-client-benchmark-download.php @@ -55,4 +55,6 @@ echo "\r" . 'Downloaded ' . $bytes . ' bytes in ' . round($time, 3) . 's => ' . round($bytes / $time / 1000000, 1) . ' MB/s' . PHP_EOL; }); -}, 'printf'); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); diff --git a/examples/92-client-benchmark-upload.php b/examples/92-client-benchmark-upload.php index bd767966..9fa1848a 100644 --- a/examples/92-client-benchmark-upload.php +++ b/examples/92-client-benchmark-upload.php @@ -116,7 +116,7 @@ public function getPosition() printf("\r%d bytes in %0.3fs => %.1f MB/s\n", $source->getPosition(), $now - $start, $source->getPosition() / ($now - $start) / 1000000); echo rtrim(preg_replace('/x{5,}/','x…', (string) $response->getBody()), PHP_EOL) . PHP_EOL; -}, function ($e) use ($report) { +}, function (Exception $e) use ($report) { Loop::cancelTimer($report); echo 'Error: ' . $e->getMessage() . PHP_EOL; }); diff --git a/src/Browser.php b/src/Browser.php index 657b43b0..72847f66 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -97,6 +97,8 @@ public function __construct($connector = null, $loop = null) * ```php * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { * var_dump((string)$response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -123,6 +125,8 @@ public function get($url, array $headers = array()) * json_encode($data) * )->then(function (Psr\Http\Message\ResponseInterface $response) { * var_dump(json_decode((string)$response->getBody())); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -176,6 +180,8 @@ public function post($url, array $headers = array(), $body = '') * ```php * $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $response) { * var_dump($response->getHeaders()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -200,6 +206,8 @@ public function head($url, array $headers = array()) * json_encode($data) * )->then(function (Psr\Http\Message\ResponseInterface $response) { * var_dump(json_decode((string)$response->getBody())); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -240,6 +248,8 @@ public function patch($url, array $headers = array(), $body = '') * $xml->asXML() * )->then(function (Psr\Http\Message\ResponseInterface $response) { * var_dump((string)$response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -276,6 +286,8 @@ public function put($url, array $headers = array(), $body = '') * ```php * $browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $response) { * var_dump((string)$response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -302,6 +314,8 @@ public function delete($url, array $headers = array(), $body = '') * ```php * $browser->request('OPTIONS', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { * var_dump((string)$response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -362,13 +376,15 @@ public function request($method, $url, array $headers = array(), $body = '') * echo $chunk; * }); * - * $body->on('error', function (Exception $error) { - * echo 'Error: ' . $error->getMessage() . PHP_EOL; + * $body->on('error', function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * * $body->on('close', function () { * echo '[DONE]' . PHP_EOL; * }); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -472,6 +488,8 @@ public function withTimeout($timeout) * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { * // only non-redirected responses will now end up here * var_dump($response->getHeaders()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -485,6 +503,8 @@ public function withTimeout($timeout) * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { * // any redirects will now end up here * var_dump($response->getHeaderLine('Location')); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -525,6 +545,8 @@ public function withFollowRedirects($followRedirects) * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { * // any HTTP response will now end up here * var_dump($response->getStatusCode(), $response->getReasonPhrase()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * @@ -544,7 +566,7 @@ public function withFollowRedirects($followRedirects) * $response = $e->getResponse(); * var_dump($response->getStatusCode(), $response->getReasonPhrase()); * } else { - * var_dump($e->getMessage()); + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * } * }); * ``` @@ -675,6 +697,8 @@ public function withProtocolVersion($protocolVersion) * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { * // response body will not exceed 1 MiB * var_dump($response->getHeaders(), (string) $response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * From eba1e1e976f971734909d4d228f7d3825678404a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 4 Nov 2021 16:41:55 +0100 Subject: [PATCH 075/152] Explicitly close streaming response body when body MUST be empty --- src/Io/StreamingServer.php | 1 + tests/Io/StreamingServerTest.php | 110 ++++++++++++++++++++++++++++++- 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 2c912dfa..0a35896d 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -330,6 +330,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // 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 > 101 && $code < 200) || $code === 204 || $code === 304) { + $body->close(); $body = ''; } diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 0311304e..a541de9f 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -765,7 +765,7 @@ function ($data) use (&$buffer) { $this->assertEquals('', $buffer); } - public function testRespomseBodyStreamAlreadyClosedWillSendEmptyBodyChunkedEncoded() + public function testResponseBodyStreamAlreadyClosedWillSendEmptyBodyChunkedEncoded() { $stream = new ThroughStream(); $stream->close(); @@ -1255,9 +1255,45 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); $this->assertNotContainsString("bye", $buffer); } + public function testResponseContainsNoResponseBodyForHeadRequestWithStreamingResponse() + { + $stream = new ThroughStream(); + $stream->on('close', $this->expectCallableOnce()); + + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + return new Response( + 200, + array('Content-Length' => '3'), + $stream + ); + }); + + $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 = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); + } + public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContentStatus() { $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { @@ -1287,10 +1323,45 @@ function ($data) use (&$buffer) { $this->connection->emit('data', array($data)); $this->assertContainsString("HTTP/1.1 204 No Content\r\n", $buffer); - $this->assertNotContainsString("\r\n\Content-Length: 3\r\n", $buffer); + $this->assertNotContainsString("\r\nContent-Length: 3\r\n", $buffer); $this->assertNotContainsString("bye", $buffer); } + public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContentStatusResponseWithStreamingBody() + { + $stream = new ThroughStream(); + $stream->on('close', $this->expectCallableOnce()); + + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + return new Response( + 204, + array('Content-Length' => '3'), + $stream + ); + }); + + $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 = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertContainsString("HTTP/1.1 204 No Content\r\n", $buffer); + $this->assertNotContainsString("\r\nContent-Length: 3\r\n", $buffer); + } + public function testResponseContainsNoResponseBodyForNotModifiedStatus() { $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { @@ -1324,6 +1395,41 @@ function ($data) use (&$buffer) { $this->assertNotContainsString("bye", $buffer); } + public function testResponseContainsNoResponseBodyForNotModifiedStatusWithStreamingBody() + { + $stream = new ThroughStream(); + $stream->on('close', $this->expectCallableOnce()); + + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + return new Response( + 304, + array('Content-Length' => '3'), + $stream + ); + }); + + $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 = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); + $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); + } + public function testRequestInvalidHttpProtocolVersionWillEmitErrorAndSendErrorResponse() { $error = null; From 5f795a0d6d97dc624c5a9a79308d6e28f59db0d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 5 Nov 2021 08:57:40 +0100 Subject: [PATCH 076/152] Improve assigning `Content-Length` for `304 Not Modified` response --- src/Io/StreamingServer.php | 16 +++++--- tests/Io/StreamingServerTest.php | 64 ++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 0a35896d..5f9632e9 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -260,23 +260,27 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt $response = $response->withoutHeader('Date'); } - // assign "Content-Length" and "Transfer-Encoding" headers automatically + // assign "Content-Length" header automatically $chunked = false; if (($method === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === 204) { // 2xx response to CONNECT and 1xx and 204 MUST NOT include Content-Length or Transfer-Encoding header - $response = $response->withoutHeader('Content-Length')->withoutHeader('Transfer-Encoding'); + $response = $response->withoutHeader('Content-Length'); + } elseif ($code === 304 && ($response->hasHeader('Content-Length') || $body->getSize() === 0)) { + // 304 Not Modified: preserve explicit Content-Length and preserve missing header if body is empty } elseif ($body->getSize() !== null) { // assign Content-Length header when using a "normal" buffered body string - $response = $response->withHeader('Content-Length', (string)$body->getSize())->withoutHeader('Transfer-Encoding'); + $response = $response->withHeader('Content-Length', (string)$body->getSize()); } elseif (!$response->hasHeader('Content-Length') && $version === '1.1') { // assign chunked transfer-encoding if no 'content-length' is given for HTTP/1.1 responses - $response = $response->withHeader('Transfer-Encoding', 'chunked'); $chunked = true; + } + + // assign "Transfer-Encoding" header automatically + if ($chunked) { + $response = $response->withHeader('Transfer-Encoding', 'chunked'); } else { // remove any Transfer-Encoding headers unless automatically enabled above - // we do not want to keep connection alive, so pretend we received "Connection: close" request header $response = $response->withoutHeader('Transfer-Encoding'); - $request = $request->withHeader('Connection', 'close'); } // assign "Connection" header automatically diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index a541de9f..ccafe338 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -1362,6 +1362,70 @@ function ($data) use (&$buffer) { $this->assertNotContainsString("\r\nContent-Length: 3\r\n", $buffer); } + public function testResponseContainsNoContentLengthHeaderForNotModifiedStatus() + { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + return new Response( + 304, + array(), + '' + ); + }); + + $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 = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); + $this->assertNotContainsString("\r\nContent-Length: 0\r\n", $buffer); + } + + public function testResponseContainsExplicitContentLengthHeaderForNotModifiedStatus() + { + $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + return new Response( + 304, + array('Content-Length' => 3), + '' + ); + }); + + $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 = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); + $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); + } + public function testResponseContainsNoResponseBodyForNotModifiedStatus() { $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { From 9ed03c4fcc0573b9c9043b2f4824ed22fe9ef845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 3 Dec 2020 09:43:36 +0100 Subject: [PATCH 077/152] Internal refactoring and improvement to hold message body in memory --- src/Io/BufferedBody.php | 3 +++ src/Io/MultipartParser.php | 9 ++++----- src/Io/Transaction.php | 2 +- src/Message/Response.php | 9 ++++++--- src/Message/ServerRequest.php | 7 +++++-- tests/Io/UploadedFileTest.php | 10 +++++----- tests/Message/ResponseTest.php | 12 ++++++++---- 7 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/Io/BufferedBody.php b/src/Io/BufferedBody.php index 2f81bc56..4a4d8393 100644 --- a/src/Io/BufferedBody.php +++ b/src/Io/BufferedBody.php @@ -15,6 +15,9 @@ class BufferedBody implements StreamInterface private $position = 0; private $closed = false; + /** + * @param string $buffer + */ public function __construct($buffer) { $this->buffer = $buffer; diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index d868ca88..536694fd 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -3,7 +3,6 @@ namespace React\Http\Io; use Psr\Http\Message\ServerRequestInterface; -use RingCentral\Psr7; /** * [Internal] Parses a string body with "Content-Type: multipart/form-data" into structured data @@ -190,7 +189,7 @@ private function parseUploadedFile($filename, $contentType, $contents) } return new UploadedFile( - Psr7\stream_for(), + new BufferedBody(''), $size, \UPLOAD_ERR_NO_FILE, $filename, @@ -206,7 +205,7 @@ private function parseUploadedFile($filename, $contentType, $contents) // file exceeds "upload_max_filesize" ini setting if ($size > $this->uploadMaxFilesize) { return new UploadedFile( - Psr7\stream_for(), + new BufferedBody(''), $size, \UPLOAD_ERR_INI_SIZE, $filename, @@ -217,7 +216,7 @@ private function parseUploadedFile($filename, $contentType, $contents) // file exceeds MAX_FILE_SIZE value if ($this->maxFileSize !== null && $size > $this->maxFileSize) { return new UploadedFile( - Psr7\stream_for(), + new BufferedBody(''), $size, \UPLOAD_ERR_FORM_SIZE, $filename, @@ -226,7 +225,7 @@ private function parseUploadedFile($filename, $contentType, $contents) } return new UploadedFile( - Psr7\stream_for($contents), + new BufferedBody($contents), $size, \UPLOAD_ERR_OK, $filename, diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index 9449503f..7bf7008f 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -188,7 +188,7 @@ public function bufferResponse(ResponseInterface $response, $deferred) $maximumSize = $this->maximumSize; $promise = \React\Promise\Stream\buffer($stream, $maximumSize)->then( function ($body) use ($response) { - return $response->withBody(\RingCentral\Psr7\stream_for($body)); + return $response->withBody(new BufferedBody($body)); }, function ($e) use ($stream, $maximumSize) { // try to close stream if buffering fails (or is cancelled) diff --git a/src/Message/Response.php b/src/Message/Response.php index 5d799c58..a9710170 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -2,10 +2,11 @@ namespace React\Http\Message; +use Psr\Http\Message\StreamInterface; +use React\Http\Io\BufferedBody; use React\Http\Io\HttpBodyStream; use React\Stream\ReadableStreamInterface; use RingCentral\Psr7\Response as Psr7Response; -use Psr\Http\Message\StreamInterface; /** * Represents an outgoing server response message. @@ -48,9 +49,11 @@ public function __construct( $version = '1.1', $reason = null ) { - if ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) { + if (\is_string($body)) { + $body = new BufferedBody($body); + } elseif ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) { $body = new HttpBodyStream($body, null); - } elseif (!\is_string($body) && !$body instanceof StreamInterface) { + } elseif (!$body instanceof StreamInterface) { throw new \InvalidArgumentException('Invalid response body given'); } diff --git a/src/Message/ServerRequest.php b/src/Message/ServerRequest.php index 5c01819b..f446f24e 100644 --- a/src/Message/ServerRequest.php +++ b/src/Message/ServerRequest.php @@ -5,6 +5,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; +use React\Http\Io\BufferedBody; use React\Http\Io\HttpBodyStream; use React\Stream\ReadableStreamInterface; use RingCentral\Psr7\Request; @@ -57,10 +58,12 @@ public function __construct( $serverParams = array() ) { $stream = null; - if ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) { + if (\is_string($body)) { + $body = new BufferedBody($body); + } elseif ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) { $stream = $body; $body = null; - } elseif (!\is_string($body) && !$body instanceof StreamInterface) { + } elseif (!$body instanceof StreamInterface) { throw new \InvalidArgumentException('Invalid server request body given'); } diff --git a/tests/Io/UploadedFileTest.php b/tests/Io/UploadedFileTest.php index 9ff623da..4e9c0dd5 100644 --- a/tests/Io/UploadedFileTest.php +++ b/tests/Io/UploadedFileTest.php @@ -2,9 +2,9 @@ namespace React\Tests\Http\Io; +use React\Http\Io\BufferedBody; use React\Http\Io\UploadedFile; Use React\Tests\Http\TestCase; -use RingCentral\Psr7\BufferStream; class UploadedFileTest extends TestCase { @@ -23,7 +23,7 @@ public function failtyErrorProvider() */ public function testFailtyError($error) { - $stream = new BufferStream(); + $stream = new BufferedBody(''); $this->setExpectedException('InvalidArgumentException', 'Invalid error code, must be an UPLOAD_ERR_* constant'); new UploadedFile($stream, 0, $error, 'foo.bar', 'foo/bar'); @@ -31,7 +31,7 @@ public function testFailtyError($error) public function testNoMoveFile() { - $stream = new BufferStream(); + $stream = new BufferedBody(''); $uploadedFile = new UploadedFile($stream, 0, UPLOAD_ERR_OK, 'foo.bar', 'foo/bar'); $this->setExpectedException('RuntimeException', 'Not implemented'); @@ -40,7 +40,7 @@ public function testNoMoveFile() public function testGetters() { - $stream = new BufferStream(); + $stream = new BufferedBody(''); $uploadedFile = new UploadedFile($stream, 0, UPLOAD_ERR_OK, 'foo.bar', 'foo/bar'); self::assertSame($stream, $uploadedFile->getStream()); self::assertSame(0, $uploadedFile->getSize()); @@ -51,7 +51,7 @@ public function testGetters() public function testGetStreamOnFailedUpload() { - $stream = new BufferStream(); + $stream = new BufferedBody(''); $uploadedFile = new UploadedFile($stream, 0, UPLOAD_ERR_NO_FILE, 'foo.bar', 'foo/bar'); $this->setExpectedException('RuntimeException', 'Cannot retrieve stream due to upload error'); diff --git a/tests/Message/ResponseTest.php b/tests/Message/ResponseTest.php index 457618e9..3c6ad3fd 100644 --- a/tests/Message/ResponseTest.php +++ b/tests/Message/ResponseTest.php @@ -9,18 +9,22 @@ class ResponseTest extends TestCase { - - public function testStringBodyWillBePsr7Stream() + public function testConstructWithStringBodyWillReturnStreamInstance() { $response = new Response(200, array(), 'hello'); - $this->assertInstanceOf('RingCentral\Psr7\Stream', $response->getBody()); + $body = $response->getBody(); + + /** @var \Psr\Http\Message\StreamInterface $body */ + $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertEquals('hello', (string) $body); } public function testConstructWithStreamingBodyWillReturnReadableBodyStream() { $response = new Response(200, array(), new ThroughStream()); - $body = $response->getBody(); + + /** @var \Psr\Http\Message\StreamInterface $body */ $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); $this->assertInstanceof('React\Stream\ReadableStreamInterface', $body); $this->assertInstanceOf('React\Http\Io\HttpBodyStream', $body); From fc1cf25b7187bbcb64509fee24888ab602b23c18 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 12 Apr 2021 23:20:36 +0200 Subject: [PATCH 078/152] Introduce using PSR-7 HTTP Status code constants Introducing using these constants makes it easier to identify a specific HTTP status used in our code. And for our users to use readable status codes in their own code. --- composer.json | 3 ++- examples/51-server-hello-world.php | 3 ++- examples/52-server-count-visitors.php | 3 ++- examples/53-server-whatsmyip.php | 3 ++- examples/54-server-query-parameter.php | 3 ++- examples/55-server-cookie-handling.php | 5 +++-- examples/56-server-sleep.php | 3 ++- examples/57-server-error-handling.php | 3 ++- examples/58-server-stream-response.php | 5 +++-- examples/59-server-json-api.php | 9 +++++---- examples/61-server-hello-world-https.php | 3 ++- examples/62-server-form-upload.php | 3 ++- examples/63-server-streaming-request.php | 6 ++++-- examples/71-server-http-proxy.php | 5 +++-- examples/72-server-http-connect-proxy.php | 7 ++++--- examples/81-server-upgrade-echo.php | 5 +++-- examples/82-server-upgrade-chat.php | 5 +++-- examples/99-server-benchmark-download.php | 7 ++++--- src/Io/RequestHeaderParser.php | 11 ++++++----- src/Io/Sender.php | 3 ++- src/Io/StreamingServer.php | 13 +++++++------ src/Message/Response.php | 3 ++- 22 files changed, 67 insertions(+), 44 deletions(-) diff --git a/composer.json b/composer.json index 25f96db5..1939accf 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,8 @@ "react/promise-stream": "^1.1", "react/socket": "^1.9", "react/stream": "^1.2", - "ringcentral/psr7": "^1.2" + "ringcentral/psr7": "^1.2", + "fig/http-message-util": "^1.1" }, "require-dev": { "clue/block-react": "^1.1", diff --git a/examples/51-server-hello-world.php b/examples/51-server-hello-world.php index f549ece8..2d9dc766 100644 --- a/examples/51-server-hello-world.php +++ b/examples/51-server-hello-world.php @@ -1,5 +1,6 @@ 'text/plain' ), diff --git a/examples/52-server-count-visitors.php b/examples/52-server-count-visitors.php index d52285d0..8e219ad0 100644 --- a/examples/52-server-count-visitors.php +++ b/examples/52-server-count-visitors.php @@ -1,5 +1,6 @@ 'text/plain' ), diff --git a/examples/53-server-whatsmyip.php b/examples/53-server-whatsmyip.php index 5df1050d..14ad2da8 100644 --- a/examples/53-server-whatsmyip.php +++ b/examples/53-server-whatsmyip.php @@ -1,5 +1,6 @@ getServerParams()['REMOTE_ADDR']; return new Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/54-server-query-parameter.php b/examples/54-server-query-parameter.php index 22be7566..e3854626 100644 --- a/examples/54-server-query-parameter.php +++ b/examples/54-server-query-parameter.php @@ -1,5 +1,6 @@ 'text/html' ), diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index a6858061..7a093d24 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -1,5 +1,6 @@ getCookieParams()[$key]; return new Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -21,7 +22,7 @@ } return new Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'text/plain', 'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more') diff --git a/examples/56-server-sleep.php b/examples/56-server-sleep.php index caa22644..cfc805d3 100644 --- a/examples/56-server-sleep.php +++ b/examples/56-server-sleep.php @@ -1,5 +1,6 @@ 'text/plain' ), diff --git a/examples/57-server-error-handling.php b/examples/57-server-error-handling.php index 4a1b6757..72b9c02b 100644 --- a/examples/57-server-error-handling.php +++ b/examples/57-server-error-handling.php @@ -1,5 +1,6 @@ 'text/plain' ), diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php index 2069b7a8..596ca6fc 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -1,5 +1,6 @@ getMethod() !== 'GET' || $request->getUri()->getPath() !== '/') { - return new Response(404); + return new Response(StatusCodeInterface::STATUS_NOT_FOUND); } $stream = new ThroughStream(); @@ -30,7 +31,7 @@ }); return new Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/59-server-json-api.php b/examples/59-server-json-api.php index 7fa8cc66..8a1ba358 100644 --- a/examples/59-server-json-api.php +++ b/examples/59-server-json-api.php @@ -6,6 +6,7 @@ // $ php examples/59-server-json-api.php 8080 // $ curl -v http://localhost:8080/ -H 'Content-Type: application/json' -d '{"name":"Alice"}' +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; @@ -14,7 +15,7 @@ $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { if ($request->getHeaderLine('Content-Type') !== 'application/json') { return new Response( - 415, // Unsupported Media Type + StatusCodeInterface::STATUS_UNSUPPORTED_MEDIA_TYPE, array( 'Content-Type' => 'application/json' ), @@ -25,7 +26,7 @@ $input = json_decode($request->getBody()->getContents()); if (json_last_error() !== JSON_ERROR_NONE) { return new Response( - 400, // Bad Request + StatusCodeInterface::STATUS_BAD_REQUEST, array( 'Content-Type' => 'application/json' ), @@ -35,7 +36,7 @@ if (!isset($input->name) || !is_string($input->name)) { return new Response( - 422, // Unprocessable Entity + StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY, array( 'Content-Type' => 'application/json' ), @@ -44,7 +45,7 @@ } return new Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'application/json' ), diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index 01182fdd..f563282e 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -1,5 +1,6 @@ 'text/plain' ), diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index 6984b4e3..b09f178d 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -7,6 +7,7 @@ // $ curl --form name=test --form age=30 http://localhost:8080/ // $ curl --form name=hi --form avatar=@avatar.png http://localhost:8080/ +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\UploadedFileInterface; use React\Http\Message\Response; @@ -110,7 +111,7 @@ HTML; return new Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'text/html; charset=UTF-8' ), diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index c1e6ac89..073d0e0e 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -1,5 +1,7 @@ on('end', function () use ($resolve, &$bytes){ $resolve(new React\Http\Message\Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -31,7 +33,7 @@ function (Psr\Http\Message\ServerRequestInterface $request) { // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event $body->on('error', function (Exception $e) use ($resolve, &$bytes) { $resolve(new React\Http\Message\Response( - 400, + StatusCodeInterface::STATUS_BAD_REQUEST, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index c4fe244e..3029dd7f 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -3,6 +3,7 @@ // $ php examples/71-server-http-proxy.php 8080 // $ curl -v --proxy http://localhost:8080 http://reactphp.org/ +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\RequestInterface; use React\Http\Message\Response; use RingCentral\Psr7; @@ -16,7 +17,7 @@ $http = new React\Http\HttpServer(function (RequestInterface $request) { if (strpos($request->getRequestTarget(), '://') === false) { return new Response( - 400, + StatusCodeInterface::STATUS_BAD_REQUEST, array( 'Content-Type' => 'text/plain' ), @@ -36,7 +37,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 new Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/72-server-http-connect-proxy.php b/examples/72-server-http-connect-proxy.php index ac033370..55b03a84 100644 --- a/examples/72-server-http-connect-proxy.php +++ b/examples/72-server-http-connect-proxy.php @@ -3,6 +3,7 @@ // $ php examples/72-server-http-connect-proxy.php 8080 // $ curl -v --proxy http://localhost:8080 https://reactphp.org/ +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; use React\Socket\Connector; @@ -19,7 +20,7 @@ $http = new React\Http\HttpServer(function (ServerRequestInterface $request) use ($connector) { if ($request->getMethod() !== 'CONNECT') { return new Response( - 405, + StatusCodeInterface::STATUS_METHOD_NOT_ALLOWED, array( 'Content-Type' => 'text/plain', 'Allow' => 'CONNECT' @@ -33,14 +34,14 @@ function (ConnectionInterface $remote) { // connection established => forward data return new Response( - 200, + StatusCodeInterface::STATUS_OK, array(), $remote ); }, function (Exception $e) { return new Response( - 502, + StatusCodeInterface::STATUS_BAD_GATEWAY, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/81-server-upgrade-echo.php b/examples/81-server-upgrade-echo.php index 2f77172f..4fa54def 100644 --- a/examples/81-server-upgrade-echo.php +++ b/examples/81-server-upgrade-echo.php @@ -17,6 +17,7 @@ < world */ +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\Message\Response; @@ -30,7 +31,7 @@ $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { if ($request->getHeaderLine('Upgrade') !== 'echo' || $request->getProtocolVersion() === '1.0') { return new Response( - 426, + StatusCodeInterface::STATUS_UPGRADE_REQUIRED, array( 'Upgrade' => 'echo' ), @@ -48,7 +49,7 @@ }); return new Response( - 101, + StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS, array( 'Upgrade' => 'echo' ), diff --git a/examples/82-server-upgrade-chat.php b/examples/82-server-upgrade-chat.php index 42635e8c..84117203 100644 --- a/examples/82-server-upgrade-chat.php +++ b/examples/82-server-upgrade-chat.php @@ -19,6 +19,7 @@ Hint: try this with multiple connections :) */ +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\Message\Response; @@ -38,7 +39,7 @@ $http = new React\Http\HttpServer(function (ServerRequestInterface $request) use ($chat) { if ($request->getHeaderLine('Upgrade') !== 'chat' || $request->getProtocolVersion() === '1.0') { return new Response( - 426, + StatusCodeInterface::STATUS_UPGRADE_REQUIRED, array( 'Upgrade' => 'chat' ), @@ -76,7 +77,7 @@ }); return new Response( - 101, + StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS, array( 'Upgrade' => 'chat' ), diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index df0e69e7..f7117aa5 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -15,6 +15,7 @@ // $ docker run -it --rm --net=host skandyla/wrk -t8 -c10 -d20 http://localhost:8080/ use Evenement\EventEmitter; +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; use React\Stream\ReadableStreamInterface; @@ -94,7 +95,7 @@ public function getSize() switch ($request->getUri()->getPath()) { case '/': return new Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'text/html' ), @@ -107,13 +108,13 @@ public function getSize() $stream = new ChunkRepeater(str_repeat('.', 1000000), 10000); break; default: - return new Response(404); + return new Response(StatusCodeInterface::STATUS_NOT_FOUND); } React\EventLoop\Loop::addTimer(0, array($stream, 'resume')); return new Response( - 200, + StatusCodeInterface::STATUS_OK, array( 'Content-Type' => 'application/octet-data', 'Content-Length' => $stream->getSize() diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 64b5dcdb..a6136e2f 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -3,6 +3,7 @@ namespace React\Http\Io; use Evenement\EventEmitter; +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\ServerRequest; use React\Socket\ConnectionInterface; @@ -39,7 +40,7 @@ public function handle(ConnectionInterface $conn) $fn = null; $that->emit('error', array( - new \OverflowException("Maximum header size of {$maxSize} exceeded.", 431), + new \OverflowException("Maximum header size of {$maxSize} exceeded.", StatusCodeInterface::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE), $conn )); return; @@ -127,7 +128,7 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) // 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', 505); + throw new \InvalidArgumentException('Received request with invalid protocol version', StatusCodeInterface::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 @@ -256,20 +257,20 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) // 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', 501); + throw new \InvalidArgumentException('Only chunked-encoding is allowed for Transfer-Encoding', StatusCodeInterface::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', 400); + throw new \InvalidArgumentException('Using both `Transfer-Encoding: chunked` and `Content-Length` is not allowed', StatusCodeInterface::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', 400); + throw new \InvalidArgumentException('The value of `Content-Length` is not valid', StatusCodeInterface::STATUS_BAD_REQUEST); } } diff --git a/src/Io/Sender.php b/src/Io/Sender.php index c1bbab42..6e2d661e 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -2,6 +2,7 @@ namespace React\Http\Io; +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use React\EventLoop\LoopInterface; @@ -110,7 +111,7 @@ public function send(RequestInterface $request) $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 == 204 || $code == 304) { + if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code == StatusCodeInterface::STATUS_NO_CONTENT || $code == StatusCodeInterface::STATUS_NOT_MODIFIED) { $length = 0; } elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') { $body = new ChunkedDecoder($body); diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 5f9632e9..c7039f50 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -3,6 +3,7 @@ namespace React\Http\Io; use Evenement\EventEmitter; +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; @@ -120,7 +121,7 @@ public function __construct(LoopInterface $loop, $requestHandler) // parsing failed => assume dummy request and send appropriate error $that->writeError( $conn, - $e->getCode() !== 0 ? $e->getCode() : 400, + $e->getCode() !== 0 ? $e->getCode() : StatusCodeInterface::STATUS_BAD_REQUEST, new ServerRequest('GET', '/') ); }); @@ -199,7 +200,7 @@ function ($error) use ($that, $conn, $request) { $exception = new \RuntimeException($message, null, $previous); $that->emit('error', array($exception)); - return $that->writeError($conn, 500, $request); + return $that->writeError($conn, StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR, $request); } ); } @@ -262,7 +263,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // assign "Content-Length" header automatically $chunked = false; - if (($method === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === 204) { + if (($method === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === StatusCodeInterface::STATUS_NO_CONTENT) { // 2xx response to CONNECT and 1xx and 204 MUST NOT include Content-Length or Transfer-Encoding header $response = $response->withoutHeader('Content-Length'); } elseif ($code === 304 && ($response->hasHeader('Content-Length') || $body->getSize() === 0)) { @@ -285,7 +286,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // assign "Connection" header automatically $persist = false; - if ($code === 101) { + if ($code === StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS) { // 101 (Switching Protocols) response uses Connection: upgrade header // This implies that this stream now uses another protocol and we // may not persist this connection for additional requests. @@ -307,7 +308,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // 101 (Switching Protocols) response (for Upgrade request) forwards upgraded data through duplex stream // 2xx (Successful) response to CONNECT forwards tunneled application data through duplex stream - if (($code === 101 || ($method === 'CONNECT' && $code >= 200 && $code < 300)) && $body instanceof HttpBodyStream && $body->input instanceof WritableStreamInterface) { + if (($code === StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS || ($method === 'CONNECT' && $code >= 200 && $code < 300)) && $body instanceof HttpBodyStream && $body->input instanceof WritableStreamInterface) { if ($request->getBody()->isReadable()) { // request is still streaming => wait for request close before forwarding following data from connection $request->getBody()->on('close', function () use ($connection, $body) { @@ -333,7 +334,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // 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 > 101 && $code < 200) || $code === 204 || $code === 304) { + if ($method === 'HEAD' || $code === 100 || ($code > StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS && $code < 200) || $code === StatusCodeInterface::STATUS_NO_CONTENT || $code === StatusCodeInterface::STATUS_NOT_MODIFIED) { $body->close(); $body = ''; } diff --git a/src/Message/Response.php b/src/Message/Response.php index a9710170..46c91501 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -2,6 +2,7 @@ namespace React\Http\Message; +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\StreamInterface; use React\Http\Io\BufferedBody; use React\Http\Io\HttpBodyStream; @@ -43,7 +44,7 @@ final class Response extends Psr7Response * @throws \InvalidArgumentException for an invalid body */ public function __construct( - $status = 200, + $status = StatusCodeInterface::STATUS_OK, array $headers = array(), $body = '', $version = '1.1', From 50af5634826dba7bfa3a243750402d306bd3a490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 26 Sep 2021 19:19:35 +0200 Subject: [PATCH 079/152] Expose all status code constants via `Response` class --- README.md | 71 +++++++++++++---------- composer.json | 4 +- examples/51-server-hello-world.php | 3 +- examples/52-server-count-visitors.php | 3 +- examples/53-server-whatsmyip.php | 3 +- examples/54-server-query-parameter.php | 3 +- examples/55-server-cookie-handling.php | 5 +- examples/56-server-sleep.php | 3 +- examples/57-server-error-handling.php | 3 +- examples/58-server-stream-response.php | 5 +- examples/59-server-json-api.php | 9 ++- examples/61-server-hello-world-https.php | 3 +- examples/62-server-form-upload.php | 3 +- examples/63-server-streaming-request.php | 6 +- examples/71-server-http-proxy.php | 5 +- examples/72-server-http-connect-proxy.php | 7 +-- examples/81-server-upgrade-echo.php | 5 +- examples/82-server-upgrade-chat.php | 5 +- examples/99-server-benchmark-download.php | 7 +-- src/HttpServer.php | 2 +- src/Io/RequestHeaderParser.php | 12 ++-- src/Io/Sender.php | 4 +- src/Io/StreamingServer.php | 19 +++--- src/Message/Response.php | 15 +++-- 24 files changed, 100 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index 23010002..50797fe6 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ require __DIR__ . '/vendor/autoload.php'; $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -735,7 +735,7 @@ object and expects a [response](#server-response) object in return: ```php $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -953,7 +953,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $body .= "The requested path is: " . $request->getUri()->getPath(); return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -995,7 +995,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -1027,7 +1027,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf } return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/html' ), @@ -1074,7 +1074,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $name = $request->getParsedBody()['name'] ?? 'anonymous'; return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array(), "Hello $name!\n" ); @@ -1099,7 +1099,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $name = $data->name ?? 'anonymous'; return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array('Content-Type' => 'application/json'), json_encode(['message' => "Hello $name!"]) ); @@ -1122,7 +1122,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $name = isset($files['avatar']) ? $files['avatar']->getClientFilename() : 'nothing'; return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array(), "Uploaded $name\n" ); @@ -1205,7 +1205,7 @@ $http = new React\Http\HttpServer( $body->on('end', function () use ($resolve, &$bytes){ $resolve(new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -1216,7 +1216,7 @@ $http = new React\Http\HttpServer( // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event $body->on('error', function (Exception $e) use ($resolve, &$bytes) { $resolve(new React\Http\Message\Response( - 400, + React\Http\Message\Response::STATUS_BAD_REQUEST, array( 'Content-Type' => 'text/plain' ), @@ -1272,7 +1272,7 @@ $http = new React\Http\HttpServer( $body .= 'This example does not accept chunked transfer encoding.'; return new React\Http\Message\Response( - 411, + React\Http\Message\Response::STATUS_LENGTH_REQUIRED, array( 'Content-Type' => 'text/plain' ), @@ -1281,7 +1281,7 @@ $http = new React\Http\HttpServer( } return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -1343,7 +1343,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $body = "Your cookie value is: " . $request->getCookieParams()[$key]; return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -1352,7 +1352,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf } return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain', 'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more') @@ -1410,7 +1410,7 @@ In its most simple form, you can use it like this: ```php $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -1440,7 +1440,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf return new Promise(function ($resolve, $reject) { Loop::addTimer(1.5, function() use ($resolve) { $response = new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -1487,7 +1487,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf }); return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -1568,7 +1568,7 @@ a `string` response body like this: ```php $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -1593,7 +1593,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf }); return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Length' => '13', 'Content-Type' => 'text/plain', @@ -1663,7 +1663,7 @@ a custom `Server` response header like this: ```php $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Server' => 'PHP/3' ) @@ -1678,7 +1678,7 @@ string value like this: ```php $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Server' => '' ) @@ -1693,7 +1693,7 @@ like this: ```php $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Date' => gmdate('D, d M Y H:i:s \G\M\T') ) @@ -1708,7 +1708,7 @@ like this: ```php $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Date' => '' ) @@ -1786,7 +1786,7 @@ encourages [Third-Party Middleware](#third-party-middleware) implementations. In order to use middleware request handlers, simply pass a list of all callables as defined above to the [`HttpServer`](#httpserver). The following example adds a middleware request handler that adds the current time to the request as a -header (`Request-Time`) and a final request handler that always returns a 200 code without a body: +header (`Request-Time`) and a final request handler that always returns a `200 OK` status code without a body: ```php $http = new React\Http\HttpServer( @@ -1795,7 +1795,7 @@ $http = new React\Http\HttpServer( return $next($request); }, function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Message\Response(200); + return new React\Http\Message\Response(React\Http\Message\Response::STATUS_OK); } ); ``` @@ -1821,7 +1821,7 @@ $http = new React\Http\HttpServer( }); }, function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Message\Response(200); + return new React\Http\Message\Response(React\Http\Message\Response::STATUS_OK); } ); ``` @@ -1842,7 +1842,7 @@ $http = new React\Http\HttpServer( }); return $promise->then(null, function (Exception $e) { return new React\Http\Message\Response( - 500, + React\Http\Message\Response::STATUS_INTERNAL_SERVER_ERROR, array(), 'Internal error: ' . $e->getMessage() ); @@ -1852,7 +1852,7 @@ $http = new React\Http\HttpServer( if (mt_rand(0, 1) === 1) { throw new RuntimeException('Database error'); } - return new React\Http\Message\Response(200); + return new React\Http\Message\Response(React\Http\Message\Response::STATUS_OK); } ); ``` @@ -2439,7 +2439,7 @@ represent an outgoing server response message. ```php $response = new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/html' ), @@ -2452,6 +2452,13 @@ This class implements the which in turn extends the [PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). +On top of this, this class implements the +[PSR-7 Message Util `StatusCodeInterface`](https://github.com/php-fig/http-message-util/blob/master/src/StatusCodeInterface.php) +which means that most common HTTP status codes are available as class +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 considered an implementation detail that may change in the future. @@ -2516,7 +2523,7 @@ $http = new React\Http\HttpServer( }); $body->on('close', function () use (&$bytes, $resolve) { $resolve(new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, [], "Received $bytes bytes\n" )); @@ -2653,7 +2660,7 @@ $http = new React\Http\HttpServer( new React\Http\Middleware\RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (Psr\Http\Message\ServerRequestInterface $request) { // The body from $request->getBody() is now fully available without the need to stream it - return new React\Http\Message\Response(200); + return new React\Http\Message\Response(React\Http\Message\Response::STATUS_OK); }, ); ``` @@ -2700,7 +2707,7 @@ $handler = function (Psr\Http\Message\ServerRequestInterface $request) { } return new React\Http\Message\Response( - 200, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), diff --git a/composer.json b/composer.json index 1939accf..8a5c7df6 100644 --- a/composer.json +++ b/composer.json @@ -28,14 +28,14 @@ "require": { "php": ">=5.3.0", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "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", - "fig/http-message-util": "^1.1" + "ringcentral/psr7": "^1.2" }, "require-dev": { "clue/block-react": "^1.1", diff --git a/examples/51-server-hello-world.php b/examples/51-server-hello-world.php index 2d9dc766..88831525 100644 --- a/examples/51-server-hello-world.php +++ b/examples/51-server-hello-world.php @@ -1,6 +1,5 @@ 'text/plain' ), diff --git a/examples/52-server-count-visitors.php b/examples/52-server-count-visitors.php index 8e219ad0..bdd53af9 100644 --- a/examples/52-server-count-visitors.php +++ b/examples/52-server-count-visitors.php @@ -1,6 +1,5 @@ 'text/plain' ), diff --git a/examples/53-server-whatsmyip.php b/examples/53-server-whatsmyip.php index 14ad2da8..e0835a82 100644 --- a/examples/53-server-whatsmyip.php +++ b/examples/53-server-whatsmyip.php @@ -1,6 +1,5 @@ getServerParams()['REMOTE_ADDR']; return new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/54-server-query-parameter.php b/examples/54-server-query-parameter.php index e3854626..18dd56b0 100644 --- a/examples/54-server-query-parameter.php +++ b/examples/54-server-query-parameter.php @@ -1,6 +1,5 @@ 'text/html' ), diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index 7a093d24..8260fc33 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -1,6 +1,5 @@ getCookieParams()[$key]; return new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -22,7 +21,7 @@ } return new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array( 'Content-Type' => 'text/plain', 'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more') diff --git a/examples/56-server-sleep.php b/examples/56-server-sleep.php index cfc805d3..6bb6f82b 100644 --- a/examples/56-server-sleep.php +++ b/examples/56-server-sleep.php @@ -1,6 +1,5 @@ 'text/plain' ), diff --git a/examples/57-server-error-handling.php b/examples/57-server-error-handling.php index 72b9c02b..71cbad15 100644 --- a/examples/57-server-error-handling.php +++ b/examples/57-server-error-handling.php @@ -1,6 +1,5 @@ 'text/plain' ), diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php index 596ca6fc..015ddd9a 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -1,6 +1,5 @@ getMethod() !== 'GET' || $request->getUri()->getPath() !== '/') { - return new Response(StatusCodeInterface::STATUS_NOT_FOUND); + return new Response(Response::STATUS_NOT_FOUND); } $stream = new ThroughStream(); @@ -31,7 +30,7 @@ }); return new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/59-server-json-api.php b/examples/59-server-json-api.php index 8a1ba358..0d50b52b 100644 --- a/examples/59-server-json-api.php +++ b/examples/59-server-json-api.php @@ -6,7 +6,6 @@ // $ php examples/59-server-json-api.php 8080 // $ curl -v http://localhost:8080/ -H 'Content-Type: application/json' -d '{"name":"Alice"}' -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; @@ -15,7 +14,7 @@ $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { if ($request->getHeaderLine('Content-Type') !== 'application/json') { return new Response( - StatusCodeInterface::STATUS_UNSUPPORTED_MEDIA_TYPE, + Response::STATUS_UNSUPPORTED_MEDIA_TYPE, array( 'Content-Type' => 'application/json' ), @@ -26,7 +25,7 @@ $input = json_decode($request->getBody()->getContents()); if (json_last_error() !== JSON_ERROR_NONE) { return new Response( - StatusCodeInterface::STATUS_BAD_REQUEST, + Response::STATUS_BAD_REQUEST, array( 'Content-Type' => 'application/json' ), @@ -36,7 +35,7 @@ if (!isset($input->name) || !is_string($input->name)) { return new Response( - StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY, + Response::STATUS_UNPROCESSABLE_ENTITY, array( 'Content-Type' => 'application/json' ), @@ -45,7 +44,7 @@ } return new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array( 'Content-Type' => 'application/json' ), diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index f563282e..2fd6f9af 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -1,6 +1,5 @@ 'text/plain' ), diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index b09f178d..899caa0a 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -7,7 +7,6 @@ // $ curl --form name=test --form age=30 http://localhost:8080/ // $ curl --form name=hi --form avatar=@avatar.png http://localhost:8080/ -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\UploadedFileInterface; use React\Http\Message\Response; @@ -111,7 +110,7 @@ HTML; return new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array( 'Content-Type' => 'text/html; charset=UTF-8' ), diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index 073d0e0e..b20b8f08 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -1,7 +1,5 @@ on('end', function () use ($resolve, &$bytes){ $resolve(new React\Http\Message\Response( - StatusCodeInterface::STATUS_OK, + React\Http\Message\Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), @@ -33,7 +31,7 @@ function (Psr\Http\Message\ServerRequestInterface $request) { // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event $body->on('error', function (Exception $e) use ($resolve, &$bytes) { $resolve(new React\Http\Message\Response( - StatusCodeInterface::STATUS_BAD_REQUEST, + React\Http\Message\Response::STATUS_BAD_REQUEST, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index 3029dd7f..e0bf8404 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -3,7 +3,6 @@ // $ php examples/71-server-http-proxy.php 8080 // $ curl -v --proxy http://localhost:8080 http://reactphp.org/ -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\RequestInterface; use React\Http\Message\Response; use RingCentral\Psr7; @@ -17,7 +16,7 @@ $http = new React\Http\HttpServer(function (RequestInterface $request) { if (strpos($request->getRequestTarget(), '://') === false) { return new Response( - StatusCodeInterface::STATUS_BAD_REQUEST, + Response::STATUS_BAD_REQUEST, array( 'Content-Type' => 'text/plain' ), @@ -37,7 +36,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 new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/72-server-http-connect-proxy.php b/examples/72-server-http-connect-proxy.php index 55b03a84..0500822a 100644 --- a/examples/72-server-http-connect-proxy.php +++ b/examples/72-server-http-connect-proxy.php @@ -3,7 +3,6 @@ // $ php examples/72-server-http-connect-proxy.php 8080 // $ curl -v --proxy http://localhost:8080 https://reactphp.org/ -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; use React\Socket\Connector; @@ -20,7 +19,7 @@ $http = new React\Http\HttpServer(function (ServerRequestInterface $request) use ($connector) { if ($request->getMethod() !== 'CONNECT') { return new Response( - StatusCodeInterface::STATUS_METHOD_NOT_ALLOWED, + Response::STATUS_METHOD_NOT_ALLOWED, array( 'Content-Type' => 'text/plain', 'Allow' => 'CONNECT' @@ -34,14 +33,14 @@ function (ConnectionInterface $remote) { // connection established => forward data return new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array(), $remote ); }, function (Exception $e) { return new Response( - StatusCodeInterface::STATUS_BAD_GATEWAY, + Response::STATUS_BAD_GATEWAY, array( 'Content-Type' => 'text/plain' ), diff --git a/examples/81-server-upgrade-echo.php b/examples/81-server-upgrade-echo.php index 4fa54def..cd3dc156 100644 --- a/examples/81-server-upgrade-echo.php +++ b/examples/81-server-upgrade-echo.php @@ -17,7 +17,6 @@ < world */ -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\Message\Response; @@ -31,7 +30,7 @@ $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { if ($request->getHeaderLine('Upgrade') !== 'echo' || $request->getProtocolVersion() === '1.0') { return new Response( - StatusCodeInterface::STATUS_UPGRADE_REQUIRED, + Response::STATUS_UPGRADE_REQUIRED, array( 'Upgrade' => 'echo' ), @@ -49,7 +48,7 @@ }); return new Response( - StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS, + Response::STATUS_SWITCHING_PROTOCOLS, array( 'Upgrade' => 'echo' ), diff --git a/examples/82-server-upgrade-chat.php b/examples/82-server-upgrade-chat.php index 84117203..bd791fb0 100644 --- a/examples/82-server-upgrade-chat.php +++ b/examples/82-server-upgrade-chat.php @@ -19,7 +19,6 @@ Hint: try this with multiple connections :) */ -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\Message\Response; @@ -39,7 +38,7 @@ $http = new React\Http\HttpServer(function (ServerRequestInterface $request) use ($chat) { if ($request->getHeaderLine('Upgrade') !== 'chat' || $request->getProtocolVersion() === '1.0') { return new Response( - StatusCodeInterface::STATUS_UPGRADE_REQUIRED, + Response::STATUS_UPGRADE_REQUIRED, array( 'Upgrade' => 'chat' ), @@ -77,7 +76,7 @@ }); return new Response( - StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS, + Response::STATUS_SWITCHING_PROTOCOLS, array( 'Upgrade' => 'chat' ), diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index f7117aa5..6c737605 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -15,7 +15,6 @@ // $ docker run -it --rm --net=host skandyla/wrk -t8 -c10 -d20 http://localhost:8080/ use Evenement\EventEmitter; -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; use React\Stream\ReadableStreamInterface; @@ -95,7 +94,7 @@ public function getSize() switch ($request->getUri()->getPath()) { case '/': return new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array( 'Content-Type' => 'text/html' ), @@ -108,13 +107,13 @@ public function getSize() $stream = new ChunkRepeater(str_repeat('.', 1000000), 10000); break; default: - return new Response(StatusCodeInterface::STATUS_NOT_FOUND); + return new Response(Response::STATUS_NOT_FOUND); } React\EventLoop\Loop::addTimer(0, array($stream, 'resume')); return new Response( - StatusCodeInterface::STATUS_OK, + Response::STATUS_OK, array( 'Content-Type' => 'application/octet-data', 'Content-Length' => $stream->getSize() diff --git a/src/HttpServer.php b/src/HttpServer.php index 95ccc5ff..f2334733 100644 --- a/src/HttpServer.php +++ b/src/HttpServer.php @@ -26,7 +26,7 @@ * ```php * $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { * return new React\Http\Message\Response( - * 200, + * React\Http\Message\Response::STATUS_OK, * array( * 'Content-Type' => 'text/plain' * ), diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index a6136e2f..743c006c 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -3,8 +3,8 @@ namespace React\Http\Io; use Evenement\EventEmitter; -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; +use React\Http\Message\Response; use React\Http\Message\ServerRequest; use React\Socket\ConnectionInterface; use Exception; @@ -40,7 +40,7 @@ public function handle(ConnectionInterface $conn) $fn = null; $that->emit('error', array( - new \OverflowException("Maximum header size of {$maxSize} exceeded.", StatusCodeInterface::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE), + new \OverflowException("Maximum header size of {$maxSize} exceeded.", Response::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE), $conn )); return; @@ -128,7 +128,7 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) // 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', StatusCodeInterface::STATUS_VERSION_NOT_SUPPORTED); + 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 @@ -257,20 +257,20 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) // 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', StatusCodeInterface::STATUS_NOT_IMPLEMENTED); + 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', StatusCodeInterface::STATUS_BAD_REQUEST); + 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', StatusCodeInterface::STATUS_BAD_REQUEST); + throw new \InvalidArgumentException('The value of `Content-Length` is not valid', Response::STATUS_BAD_REQUEST); } } diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 6e2d661e..2f04c797 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -2,11 +2,11 @@ namespace React\Http\Io; -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\RequestInterface; 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; @@ -111,7 +111,7 @@ public function send(RequestInterface $request) $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 == StatusCodeInterface::STATUS_NO_CONTENT || $code == StatusCodeInterface::STATUS_NOT_MODIFIED) { + 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); diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index c7039f50..dd4c0584 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -3,7 +3,6 @@ namespace React\Http\Io; use Evenement\EventEmitter; -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; @@ -32,7 +31,7 @@ * ```php * $server = new StreamingServer($loop, function (ServerRequestInterface $request) { * return new Response( - * 200, + * Response::STATUS_OK, * array( * 'Content-Type' => 'text/plain' * ), @@ -121,7 +120,7 @@ public function __construct(LoopInterface $loop, $requestHandler) // parsing failed => assume dummy request and send appropriate error $that->writeError( $conn, - $e->getCode() !== 0 ? $e->getCode() : StatusCodeInterface::STATUS_BAD_REQUEST, + $e->getCode() !== 0 ? $e->getCode() : Response::STATUS_BAD_REQUEST, new ServerRequest('GET', '/') ); }); @@ -183,7 +182,7 @@ function ($response) use ($that, $conn, $request) { $exception = new \RuntimeException($message); $that->emit('error', array($exception)); - return $that->writeError($conn, 500, $request); + return $that->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); } $that->handleResponse($conn, $request, $response); }, @@ -200,7 +199,7 @@ function ($error) use ($that, $conn, $request) { $exception = new \RuntimeException($message, null, $previous); $that->emit('error', array($exception)); - return $that->writeError($conn, StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR, $request); + return $that->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); } ); } @@ -263,10 +262,10 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // assign "Content-Length" header automatically $chunked = false; - if (($method === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === StatusCodeInterface::STATUS_NO_CONTENT) { + if (($method === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === Response::STATUS_NO_CONTENT) { // 2xx response to CONNECT and 1xx and 204 MUST NOT include Content-Length or Transfer-Encoding header $response = $response->withoutHeader('Content-Length'); - } elseif ($code === 304 && ($response->hasHeader('Content-Length') || $body->getSize() === 0)) { + } elseif ($code === Response::STATUS_NOT_MODIFIED && ($response->hasHeader('Content-Length') || $body->getSize() === 0)) { // 304 Not Modified: preserve explicit Content-Length and preserve missing header if body is empty } elseif ($body->getSize() !== null) { // assign Content-Length header when using a "normal" buffered body string @@ -286,7 +285,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // assign "Connection" header automatically $persist = false; - if ($code === StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS) { + if ($code === Response::STATUS_SWITCHING_PROTOCOLS) { // 101 (Switching Protocols) response uses Connection: upgrade header // This implies that this stream now uses another protocol and we // may not persist this connection for additional requests. @@ -308,7 +307,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // 101 (Switching Protocols) response (for Upgrade request) forwards upgraded data through duplex stream // 2xx (Successful) response to CONNECT forwards tunneled application data through duplex stream - if (($code === StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS || ($method === 'CONNECT' && $code >= 200 && $code < 300)) && $body instanceof HttpBodyStream && $body->input instanceof WritableStreamInterface) { + if (($code === Response::STATUS_SWITCHING_PROTOCOLS || ($method === 'CONNECT' && $code >= 200 && $code < 300)) && $body instanceof HttpBodyStream && $body->input instanceof WritableStreamInterface) { if ($request->getBody()->isReadable()) { // request is still streaming => wait for request close before forwarding following data from connection $request->getBody()->on('close', function () use ($connection, $body) { @@ -334,7 +333,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // 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 > StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS && $code < 200) || $code === StatusCodeInterface::STATUS_NO_CONTENT || $code === StatusCodeInterface::STATUS_NOT_MODIFIED) { + if ($method === 'HEAD' || ($code >= 100 && $code < 200 && $code !== Response::STATUS_SWITCHING_PROTOCOLS) || $code === Response::STATUS_NO_CONTENT || $code === Response::STATUS_NOT_MODIFIED) { $body->close(); $body = ''; } diff --git a/src/Message/Response.php b/src/Message/Response.php index 46c91501..0ce8bef0 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -14,7 +14,7 @@ * * ```php * $response = new React\Http\Message\Response( - * 200, + * React\Http\Message\Response::STATUS_OK, * array( * 'Content-Type' => 'text/html' * ), @@ -27,16 +27,23 @@ * which in turn extends the * [PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). * + * On top of this, this class implements the + * [PSR-7 Message Util `StatusCodeInterface`](https://github.com/php-fig/http-message-util/blob/master/src/StatusCodeInterface.php) + * which means that most common HTTP status codes are available as class + * 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 * considered an implementation detail that may change in the future. * * @see \Psr\Http\Message\ResponseInterface */ -final class Response extends Psr7Response +final class Response extends Psr7Response implements StatusCodeInterface { /** - * @param int $status HTTP status code (e.g. 200/404) + * @param int $status HTTP status code (e.g. 200/404), see `self::STATUS_*` constants * @param array $headers additional response headers * @param string|ReadableStreamInterface|StreamInterface $body response body * @param string $version HTTP protocol version (e.g. 1.1/1.0) @@ -44,7 +51,7 @@ final class Response extends Psr7Response * @throws \InvalidArgumentException for an invalid body */ public function __construct( - $status = StatusCodeInterface::STATUS_OK, + $status = self::STATUS_OK, array $headers = array(), $body = '', $version = '1.1', From 17dd30d2dad082c18c818878c598313d0cfce76a Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Mon, 15 Nov 2021 15:27:51 +0100 Subject: [PATCH 080/152] Support PHP 8.1 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf214c83..c64ef6ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: php: + - 8.1 - 8.0 - 7.4 - 7.3 From aaa6afbe6780497f0a74caf8302f1adab21d39ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 4 Dec 2021 16:31:15 +0100 Subject: [PATCH 081/152] Support PHP 8.1 --- phpunit.xml.dist | 3 ++- src/Io/RequestHeaderParser.php | 6 ++++-- src/Io/StreamingServer.php | 2 +- tests/Io/IniUtilTest.php | 1 - 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fa88e7e0..fd6a9234 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -5,7 +5,8 @@ xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" - cacheResult="false"> + cacheResult="false" + convertDeprecationsToExceptions="true"> ./tests/ diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 743c006c..e5554c46 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -160,7 +160,7 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) ); // scheme is `http` unless TLS is used - $localParts = \parse_url(https://github.com/$localSocketUri); + $localParts = $localSocketUri === null ? array() : \parse_url(https://github.com/$localSocketUri); if (isset($localParts['scheme']) && $localParts['scheme'] === 'tls') { $scheme = 'https://'; $serverParams['HTTPS'] = 'on'; @@ -242,7 +242,9 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) } // make sure value does not contain any other URI component - unset($parts['scheme'], $parts['host'], $parts['port']); + if (\is_array($parts)) { + unset($parts['scheme'], $parts['host'], $parts['port']); + } if ($parts === false || $parts) { throw new \InvalidArgumentException('Invalid Host header value'); } diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index dd4c0584..7818f0bd 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -196,7 +196,7 @@ function ($error) use ($that, $conn, $request) { $previous = $error; } - $exception = new \RuntimeException($message, null, $previous); + $exception = new \RuntimeException($message, 0, $previous); $that->emit('error', array($exception)); return $that->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); diff --git a/tests/Io/IniUtilTest.php b/tests/Io/IniUtilTest.php index 22374eb4..155c6ed2 100644 --- a/tests/Io/IniUtilTest.php +++ b/tests/Io/IniUtilTest.php @@ -63,7 +63,6 @@ public function provideInvalidInputIniSizeToBytes() return array( array('-1G'), array('0G'), - array(null), array('foo'), array('fooK'), array('1ooL'), From 5d37d3769b8c35ce1ad67f29ceb496607dc9b1e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 23 Jan 2022 11:13:33 +0100 Subject: [PATCH 082/152] Update `Host` header tests requiring root access --- phpunit.xml.dist | 2 +- tests/FunctionalHttpServerTest.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fd6a9234..93a36f6b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -4,8 +4,8 @@ diff --git a/tests/FunctionalHttpServerTest.php b/tests/FunctionalHttpServerTest.php index fe0e1936..a5274533 100644 --- a/tests/FunctionalHttpServerTest.php +++ b/tests/FunctionalHttpServerTest.php @@ -302,7 +302,7 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() $loop = Factory::create(); try { - $socket = new SocketServer('127.0.0.1:443', array('tls' => array( + $socket = new SocketServer('tls://127.0.0.1:443', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' )), $loop); } catch (\RuntimeException $e) { @@ -341,7 +341,7 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() $loop = Factory::create(); try { - $socket = new SocketServer('127.0.0.1:443', array('tls' => array( + $socket = new SocketServer('tls://127.0.0.1:443', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' )), $loop); } catch (\RuntimeException $e) { @@ -410,7 +410,7 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() $loop = Factory::create(); try { - $socket = new SocketServer('127.0.0.1:80', array('tls' => array( + $socket = new SocketServer('tls://127.0.0.1:80', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' )), $loop); } catch (\RuntimeException $e) { From 5d3013560286abdb8bb5f30072e6c8c4b662698f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 23 Jan 2022 11:38:37 +0100 Subject: [PATCH 083/152] Update test suite to use default loop --- composer.json | 2 +- tests/Client/FunctionalIntegrationTest.php | 36 ++- tests/FunctionalBrowserTest.php | 142 +++++------ tests/FunctionalHttpServerTest.php | 236 ++++++++---------- tests/HttpServerTest.php | 21 +- tests/Io/MiddlewareRunnerTest.php | 5 +- tests/Io/StreamingServerTest.php | 232 ++++++++--------- tests/Io/TransactionTest.php | 22 +- .../RequestBodyBufferMiddlewareTest.php | 20 +- 9 files changed, 329 insertions(+), 387 deletions(-) diff --git a/composer.json b/composer.json index 8a5c7df6..4c9a0383 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "ringcentral/psr7": "^1.2" }, "require-dev": { - "clue/block-react": "^1.1", + "clue/block-react": "^1.5", "clue/http-proxy-react": "^1.7", "clue/reactphp-ssh-proxy": "^1.3", "clue/socks-react": "^1.3", diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 6a62be93..64a3ea8a 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -4,7 +4,7 @@ use Clue\React\Block; use Psr\Http\Message\ResponseInterface; -use React\EventLoop\Factory; +use React\EventLoop\Loop; use React\Http\Client\Client; use React\Promise\Deferred; use React\Promise\Stream; @@ -37,9 +37,7 @@ class FunctionalIntegrationTest extends TestCase public function testRequestToLocalhostEmitsSingleRemoteConnection() { - $loop = Factory::create(); - - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $socket->on('connection', $this->expectCallableOnce()); $socket->on('connection', function (ConnectionInterface $conn) use ($socket) { $conn->end("HTTP/1.1 200 OK\r\n\r\nOk"); @@ -47,26 +45,24 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() }); $port = parse_url(https://github.com/$socket-%3EgetAddress(), PHP_URL_PORT); - $client = new Client($loop); + $client = new Client(Loop::get()); $request = $client->request('GET', 'http://localhost/' . $port); $promise = Stream\first($request, 'close'); $request->end(); - Block\await($promise, $loop, self::TIMEOUT_LOCAL); + Block\await($promise, null, self::TIMEOUT_LOCAL); } public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResponse() { - $loop = Factory::create(); - - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $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); + $client = new Client(Loop::get()); $request = $client->request('GET', str_replace('tcp:', 'http:', $socket->getAddress())); $once = $this->expectCallableOnceWith('body'); @@ -77,7 +73,7 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp $promise = Stream\first($request, 'close'); $request->end(); - Block\await($promise, $loop, self::TIMEOUT_LOCAL); + Block\await($promise, null, self::TIMEOUT_LOCAL); } /** @group internet */ @@ -86,8 +82,7 @@ public function testSuccessfulResponseEmitsEnd() // 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); - $loop = Factory::create(); - $client = new Client($loop); + $client = new Client(Loop::get()); $request = $client->request('GET', 'http://www.google.com/'); @@ -99,7 +94,7 @@ public function testSuccessfulResponseEmitsEnd() $promise = Stream\first($request, 'close'); $request->end(); - Block\await($promise, $loop, self::TIMEOUT_REMOTE); + Block\await($promise, null, self::TIMEOUT_REMOTE); } /** @group internet */ @@ -112,8 +107,7 @@ public function testPostDataReturnsData() // 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); - $loop = Factory::create(); - $client = new Client($loop); + $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))); @@ -128,7 +122,7 @@ public function testPostDataReturnsData() $request->end($data); - $buffer = Block\await($deferred->promise(), $loop, self::TIMEOUT_REMOTE); + $buffer = Block\await($deferred->promise(), null, self::TIMEOUT_REMOTE); $this->assertNotEquals('', $buffer); @@ -145,8 +139,7 @@ public function testPostJsonReturnsData() $this->markTestSkipped('Not supported on HHVM'); } - $loop = Factory::create(); - $client = new Client($loop); + $client = new Client(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')); @@ -161,7 +154,7 @@ public function testPostJsonReturnsData() $request->end($data); - $buffer = Block\await($deferred->promise(), $loop, self::TIMEOUT_REMOTE); + $buffer = Block\await($deferred->promise(), null, self::TIMEOUT_REMOTE); $this->assertNotEquals('', $buffer); @@ -176,8 +169,7 @@ 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); - $loop = Factory::create(); - $client = new Client($loop); + $client = new Client(Loop::get()); $request = $client->request('GET', 'http://www.google.com/'); $request->on('error', $this->expectCallableNever()); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 5db62003..2b7bd58c 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -5,7 +5,7 @@ use Clue\React\Block; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; +use React\EventLoop\Loop; use React\Http\Browser; use React\Http\HttpServer; use React\Http\Message\ResponseException; @@ -21,7 +21,6 @@ class FunctionalBrowserTest extends TestCase { - private $loop; private $browser; private $base; @@ -30,10 +29,9 @@ class FunctionalBrowserTest extends TestCase */ public function setUpBrowserAndServer() { - $this->loop = $loop = Factory::create(); - $this->browser = new Browser(null, $this->loop); + $this->browser = new Browser(); - $http = new HttpServer($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) use ($loop) { + $http = new HttpServer(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { $path = $request->getUri()->getPath(); $headers = array(); @@ -90,8 +88,8 @@ public function setUpBrowserAndServer() } if ($path === '/delay/10') { - return new Promise(function ($resolve) use ($loop) { - $loop->addTimer(10, function () use ($resolve) { + return new Promise(function ($resolve) { + Loop::addTimer(10, function () use ($resolve) { $resolve(new Response( 200, array(), @@ -127,7 +125,7 @@ public function setUpBrowserAndServer() if ($path === '/stream/1') { $stream = new ThroughStream(); - $loop->futureTick(function () use ($stream, $headers) { + Loop::futureTick(function () use ($stream, $headers) { $stream->end(json_encode(array( 'headers' => $headers ))); @@ -142,7 +140,7 @@ public function setUpBrowserAndServer() var_dump($path); }); - $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; @@ -153,7 +151,7 @@ public function setUpBrowserAndServer() */ public function testSimpleRequest() { - Block\await($this->browser->get($this->base . 'get'), $this->loop); + Block\await($this->browser->get($this->base . 'get')); } public function testGetRequestWithRelativeAddressRejects() @@ -161,7 +159,7 @@ public function testGetRequestWithRelativeAddressRejects() $promise = $this->browser->get('delay'); $this->setExpectedException('InvalidArgumentException', 'Invalid request URL given'); - Block\await($promise, $this->loop); + Block\await($promise); } /** @@ -169,7 +167,7 @@ public function testGetRequestWithRelativeAddressRejects() */ public function testGetRequestWithBaseAndRelativeAddressResolves() { - Block\await($this->browser->withBase($this->base)->get('get'), $this->loop); + Block\await($this->browser->withBase($this->base)->get('get')); } /** @@ -177,7 +175,7 @@ public function testGetRequestWithBaseAndRelativeAddressResolves() */ public function testGetRequestWithBaseAndFullAddressResolves() { - Block\await($this->browser->withBase('http://example.com/')->get($this->base . 'get'), $this->loop); + Block\await($this->browser->withBase('http://example.com/')->get($this->base . 'get')); } public function testCancelGetRequestWillRejectRequest() @@ -186,7 +184,7 @@ public function testCancelGetRequestWillRejectRequest() $promise->cancel(); $this->setExpectedException('RuntimeException'); - Block\await($promise, $this->loop); + Block\await($promise); } public function testCancelRequestWithPromiseFollowerWillRejectRequest() @@ -197,13 +195,13 @@ public function testCancelRequestWithPromiseFollowerWillRejectRequest() $promise->cancel(); $this->setExpectedException('RuntimeException'); - Block\await($promise, $this->loop); + Block\await($promise); } public function testRequestWithoutAuthenticationFails() { $this->setExpectedException('RuntimeException'); - Block\await($this->browser->get($this->base . 'basic-auth/user/pass'), $this->loop); + Block\await($this->browser->get($this->base . 'basic-auth/user/pass')); } /** @@ -213,7 +211,7 @@ public function testRequestWithAuthenticationSucceeds() { $base = str_replace('://', '://user:pass@', $this->base); - Block\await($this->browser->get($base . 'basic-auth/user/pass'), $this->loop); + Block\await($this->browser->get($base . 'basic-auth/user/pass')); } /** @@ -227,7 +225,7 @@ 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)), $this->loop); + Block\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($target))); } /** @@ -242,19 +240,19 @@ 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)), $this->loop); + Block\await($this->browser->get($base . 'redirect-to?url=' . urlencode($target))); } public function testCancelRedirectedRequestShouldReject() { $promise = $this->browser->get($this->base . 'redirect-to?url=delay%2F10'); - $this->loop->addTimer(0.1, function () use ($promise) { + Loop::addTimer(0.1, function () use ($promise) { $promise->cancel(); }); $this->setExpectedException('RuntimeException', 'Request cancelled'); - Block\await($promise, $this->loop); + Block\await($promise); } public function testTimeoutDelayedResponseShouldReject() @@ -262,7 +260,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, $this->loop); + Block\await($promise); } public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject() @@ -272,7 +270,7 @@ public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject() $stream->end(); $this->setExpectedException('RuntimeException', 'Request timed out after 0.1 seconds'); - Block\await($promise, $this->loop); + Block\await($promise); } /** @@ -280,7 +278,7 @@ public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject() */ public function testTimeoutFalseShouldResolveSuccessfully() { - Block\await($this->browser->withTimeout(false)->get($this->base . 'get'), $this->loop); + Block\await($this->browser->withTimeout(false)->get($this->base . 'get')); } /** @@ -288,7 +286,7 @@ public function testTimeoutFalseShouldResolveSuccessfully() */ public function testRedirectRequestRelative() { - Block\await($this->browser->get($this->base . 'redirect-to?url=get'), $this->loop); + Block\await($this->browser->get($this->base . 'redirect-to?url=get')); } /** @@ -296,7 +294,7 @@ public function testRedirectRequestRelative() */ public function testRedirectRequestAbsolute() { - Block\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($this->base . 'get')), $this->loop); + Block\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($this->base . 'get'))); } /** @@ -306,7 +304,7 @@ public function testFollowingRedirectsFalseResolvesWithRedirectResult() { $browser = $this->browser->withFollowRedirects(false); - Block\await($browser->get($this->base . 'redirect-to?url=get'), $this->loop); + Block\await($browser->get($this->base . 'redirect-to?url=get')); } public function testFollowRedirectsZeroRejectsOnRedirect() @@ -314,12 +312,12 @@ public function testFollowRedirectsZeroRejectsOnRedirect() $browser = $this->browser->withFollowRedirects(0); $this->setExpectedException('RuntimeException'); - Block\await($browser->get($this->base . 'redirect-to?url=get'), $this->loop); + Block\await($browser->get($this->base . 'redirect-to?url=get')); } public function testResponseStatus204ShouldResolveWithEmptyBody() { - $response = Block\await($this->browser->get($this->base . 'status/204'), $this->loop); + $response = Block\await($this->browser->get($this->base . 'status/204')); $this->assertFalse($response->hasHeader('Content-Length')); $body = $response->getBody(); @@ -329,7 +327,7 @@ public function testResponseStatus204ShouldResolveWithEmptyBody() public function testResponseStatus304ShouldResolveWithEmptyBodyButContentLengthResponseHeader() { - $response = Block\await($this->browser->get($this->base . 'status/304'), $this->loop); + $response = Block\await($this->browser->get($this->base . 'status/304')); $this->assertEquals('12', $response->getHeaderLine('Content-Length')); $body = $response->getBody(); @@ -344,7 +342,7 @@ public function testGetRequestWithResponseBufferMatchedExactlyResolves() { $promise = $this->browser->withResponseBuffer(5)->get($this->base . 'get'); - Block\await($promise, $this->loop); + Block\await($promise); } public function testGetRequestWithResponseBufferExceededRejects() @@ -356,7 +354,7 @@ public function testGetRequestWithResponseBufferExceededRejects() 'Response body size of 5 bytes exceeds maximum of 4 bytes', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 ); - Block\await($promise, $this->loop); + Block\await($promise); } public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() @@ -368,7 +366,7 @@ public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() 'Response body size exceeds maximum of 4 bytes', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 ); - Block\await($promise, $this->loop); + Block\await($promise); } /** @@ -381,7 +379,7 @@ public function testCanAccessHttps() $this->markTestSkipped('Not supported on HHVM'); } - Block\await($this->browser->get('https://www.google.com/'), $this->loop); + Block\await($this->browser->get('https://www.google.com/')); } /** @@ -397,12 +395,12 @@ public function testVerifyPeerEnabledForBadSslRejects() 'tls' => array( 'verify_peer' => true ) - ), $this->loop); + )); - $browser = new Browser($connector, $this->loop); + $browser = new Browser($connector); $this->setExpectedException('RuntimeException'); - Block\await($browser->get('https://self-signed.badssl.com/'), $this->loop); + Block\await($browser->get('https://self-signed.badssl.com/')); } /** @@ -419,11 +417,11 @@ public function testVerifyPeerDisabledForBadSslResolves() 'tls' => array( 'verify_peer' => false ) - ), $this->loop); + )); - $browser = new Browser($connector, $this->loop); + $browser = new Browser($connector); - Block\await($browser->get('https://self-signed.badssl.com/'), $this->loop); + Block\await($browser->get('https://self-signed.badssl.com/')); } /** @@ -432,13 +430,13 @@ public function testVerifyPeerDisabledForBadSslResolves() public function testInvalidPort() { $this->setExpectedException('RuntimeException'); - Block\await($this->browser->get('http://www.google.com:443/'), $this->loop); + Block\await($this->browser->get('http://www.google.com:443/')); } public function testErrorStatusCodeRejectsWithResponseException() { try { - Block\await($this->browser->get($this->base . 'status/404'), $this->loop); + Block\await($this->browser->get($this->base . 'status/404')); $this->fail(); } catch (ResponseException $e) { $this->assertEquals(404, $e->getCode()); @@ -450,14 +448,14 @@ public function testErrorStatusCodeRejectsWithResponseException() public function testErrorStatusCodeDoesNotRejectWithRejectErrorResponseFalse() { - $response = Block\await($this->browser->withRejectErrorResponse(false)->get($this->base . 'status/404'), $this->loop); + $response = Block\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'), $this->loop); + $response = Block\await($this->browser->post($this->base . 'post', array(), 'hello world')); $data = json_decode((string)$response->getBody(), true); $this->assertEquals('hello world', $data['data']); @@ -465,7 +463,7 @@ public function testPostString() public function testRequestStreamReturnsResponseBodyUntilConnectionsEndsForHttp10() { - $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base . 'stream/1'), $this->loop); + $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base . 'stream/1')); $this->assertEquals('1.0', $response->getProtocolVersion()); $this->assertFalse($response->hasHeader('Transfer-Encoding')); @@ -476,7 +474,7 @@ public function testRequestStreamReturnsResponseBodyUntilConnectionsEndsForHttp1 public function testRequestStreamReturnsResponseWithTransferEncodingChunkedAndResponseBodyDecodedForHttp11() { - $response = Block\await($this->browser->get($this->base . 'stream/1'), $this->loop); + $response = Block\await($this->browser->get($this->base . 'stream/1')); $this->assertEquals('1.1', $response->getProtocolVersion()); @@ -488,7 +486,7 @@ public function testRequestStreamReturnsResponseWithTransferEncodingChunkedAndRe public function testRequestStreamWithHeadRequestReturnsEmptyResponseBodWithTransferEncodingChunkedForHttp11() { - $response = Block\await($this->browser->head($this->base . 'stream/1'), $this->loop); + $response = Block\await($this->browser->head($this->base . 'stream/1')); $this->assertEquals('1.1', $response->getProtocolVersion()); @@ -498,7 +496,7 @@ public function testRequestStreamWithHeadRequestReturnsEmptyResponseBodWithTrans public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenResponseHasDoubleTransferEncoding() { - $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); + $socket = new SocketServer('127.0.0.1:0'); $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) { $connection->on('data', function () use ($connection) { $connection->end("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked, chunked\r\nConnection: close\r\n\r\nhello"); @@ -507,7 +505,7 @@ public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenRes $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = Block\await($this->browser->get($this->base . 'stream/1'), $this->loop); + $response = Block\await($this->browser->get($this->base . 'stream/1')); $this->assertEquals('1.1', $response->getProtocolVersion()); @@ -518,7 +516,7 @@ public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenRes public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeepsConnectionOpen() { $closed = new \React\Promise\Deferred(); - $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); + $socket = new SocketServer('127.0.0.1:0'); $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) use ($closed) { $connection->on('data', function () use ($connection) { $connection->write("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"); @@ -530,10 +528,10 @@ public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeeps $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = Block\await($this->browser->get($this->base . 'get', array()), $this->loop); + $response = Block\await($this->browser->get($this->base . 'get', array())); $this->assertEquals('hello', (string)$response->getBody()); - $ret = Block\await($closed->promise(), $this->loop, 0.1); + $ret = Block\await($closed->promise(), null, 0.1); $this->assertTrue($ret); $socket->close(); @@ -543,11 +541,11 @@ public function testPostStreamChunked() { $stream = new ThroughStream(); - $this->loop->addTimer(0.001, function () use ($stream) { + Loop::addTimer(0.001, function () use ($stream) { $stream->end('hello world'); }); - $response = Block\await($this->browser->post($this->base . 'post', array(), $stream), $this->loop); + $response = Block\await($this->browser->post($this->base . 'post', array(), $stream)); $data = json_decode((string)$response->getBody(), true); $this->assertEquals('hello world', $data['data']); @@ -559,11 +557,11 @@ public function testPostStreamKnownLength() { $stream = new ThroughStream(); - $this->loop->addTimer(0.001, function () use ($stream) { + Loop::addTimer(0.001, function () use ($stream) { $stream->end('hello world'); }); - $response = Block\await($this->browser->post($this->base . 'post', array('Content-Length' => 11), $stream), $this->loop); + $response = Block\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']); @@ -574,16 +572,16 @@ public function testPostStreamKnownLength() */ public function testPostStreamWillStartSendingRequestEvenWhenBodyDoesNotEmitData() { - $http = new HttpServer($this->loop, new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { + $http = new HttpServer(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { return new Response(200); }); - $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; $stream = new ThroughStream(); - Block\await($this->browser->post($this->base . 'post', array(), $stream), $this->loop); + Block\await($this->browser->post($this->base . 'post', array(), $stream)); $socket->close(); } @@ -593,7 +591,7 @@ public function testPostStreamClosed() $stream = new ThroughStream(); $stream->close(); - $response = Block\await($this->browser->post($this->base . 'post', array(), $stream), $this->loop); + $response = Block\await($this->browser->post($this->base . 'post', array(), $stream)); $data = json_decode((string)$response->getBody(), true); $this->assertEquals('', $data['data']); @@ -601,19 +599,19 @@ public function testPostStreamClosed() public function testSendsHttp11ByDefault() { - $http = new HttpServer($this->loop, function (ServerRequestInterface $request) { + $http = new HttpServer(function (ServerRequestInterface $request) { return new Response( 200, array(), $request->getProtocolVersion() ); }); - $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = Block\await($this->browser->get($this->base), $this->loop); + $response = Block\await($this->browser->get($this->base)); $this->assertEquals('1.1', (string)$response->getBody()); $socket->close(); @@ -621,19 +619,19 @@ public function testSendsHttp11ByDefault() public function testSendsExplicitHttp10Request() { - $http = new HttpServer($this->loop, function (ServerRequestInterface $request) { + $http = new HttpServer(function (ServerRequestInterface $request) { return new Response( 200, array(), $request->getProtocolVersion() ); }); - $socket = new SocketServer('127.0.0.1:0', array(), $this->loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base), $this->loop); + $response = Block\await($this->browser->withProtocolVersion('1.0')->get($this->base)); $this->assertEquals('1.0', (string)$response->getBody()); $socket->close(); @@ -641,7 +639,7 @@ public function testSendsExplicitHttp10Request() public function testHeadRequestReceivesResponseWithEmptyBodyButWithContentLengthResponseHeader() { - $response = Block\await($this->browser->head($this->base . 'get'), $this->loop); + $response = Block\await($this->browser->head($this->base . 'get')); $this->assertEquals('5', $response->getHeaderLine('Content-Length')); $body = $response->getBody(); @@ -651,7 +649,7 @@ public function testHeadRequestReceivesResponseWithEmptyBodyButWithContentLength public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndKnownSize() { - $response = Block\await($this->browser->requestStreaming('GET', $this->base . 'get'), $this->loop); + $response = Block\await($this->browser->requestStreaming('GET', $this->base . 'get')); $this->assertEquals('5', $response->getHeaderLine('Content-Length')); $body = $response->getBody(); @@ -662,7 +660,7 @@ public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndKnown public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndUnknownSizeFromStreamingEndpoint() { - $response = Block\await($this->browser->requestStreaming('GET', $this->base . 'stream/1'), $this->loop); + $response = Block\await($this->browser->requestStreaming('GET', $this->base . 'stream/1')); $this->assertFalse($response->hasHeader('Content-Length')); $body = $response->getBody(); @@ -676,8 +674,7 @@ public function testRequestStreamingGetReceivesStreamingResponseBody() $buffer = Block\await( $this->browser->requestStreaming('GET', $this->base . 'get')->then(function (ResponseInterface $response) { return Stream\buffer($response->getBody()); - }), - $this->loop + }) ); $this->assertEquals('hello', $buffer); @@ -688,8 +685,7 @@ public function testRequestStreamingGetReceivesStreamingResponseBodyEvenWhenResp $buffer = Block\await( $this->browser->withResponseBuffer(4)->requestStreaming('GET', $this->base . 'get')->then(function (ResponseInterface $response) { return Stream\buffer($response->getBody()); - }), - $this->loop + }) ); $this->assertEquals('hello', $buffer); diff --git a/tests/FunctionalHttpServerTest.php b/tests/FunctionalHttpServerTest.php index a5274533..6fa85903 100644 --- a/tests/FunctionalHttpServerTest.php +++ b/tests/FunctionalHttpServerTest.php @@ -5,7 +5,7 @@ use Clue\React\Block; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; +use React\EventLoop\Loop; use React\Http\HttpServer; use React\Http\Message\Response; use React\Http\Middleware\LimitConcurrentRequestsMiddleware; @@ -22,14 +22,13 @@ class FunctionalHttpServerTest extends TestCase { public function testPlainHttpOnRandomPort() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -38,7 +37,7 @@ public function testPlainHttpOnRandomPort() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); @@ -48,17 +47,15 @@ public function testPlainHttpOnRandomPort() public function testPlainHttpOnRandomPortWithSingleRequestHandlerArray() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); $http = new HttpServer( - $loop, function () { return new Response(404); } ); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -67,7 +64,7 @@ function () { return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 404 Not Found", $response); @@ -76,14 +73,13 @@ function () { public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -92,7 +88,7 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); @@ -102,14 +98,13 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -118,7 +113,7 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://localhost:1000/', $response); @@ -132,18 +127,17 @@ public function testSecureHttpsOnRandomPort() $this->markTestSkipped('Not supported on HHVM'); } - $loop = Factory::create(); $connector = new Connector(array( 'tls' => array('verify_peer' => false) - ), $loop); + )); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); $socket = new SocketServer('tls://127.0.0.1:0', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )), $loop); + ))); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -152,7 +146,7 @@ public function testSecureHttpsOnRandomPort() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); @@ -166,9 +160,7 @@ public function testSecureHttpsReturnsData() $this->markTestSkipped('Not supported on HHVM'); } - $loop = Factory::create(); - - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response( 200, array(), @@ -178,12 +170,12 @@ public function testSecureHttpsReturnsData() $socket = new SocketServer('tls://127.0.0.1:0', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )), $loop); + ))); $http->listen($socket); $connector = new Connector(array( 'tls' => array('verify_peer' => false) - ), $loop); + )); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); @@ -191,7 +183,7 @@ public function testSecureHttpsReturnsData() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString("\r\nContent-Length: 33000\r\n", $response); @@ -206,18 +198,17 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() $this->markTestSkipped('Not supported on HHVM'); } - $loop = Factory::create(); $connector = new Connector(array( 'tls' => array('verify_peer' => false) - ), $loop); + )); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); $socket = new SocketServer('tls://127.0.0.1:0', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )), $loop); + ))); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -226,7 +217,7 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); @@ -236,15 +227,14 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() public function testPlainHttpOnStandardPortReturnsUriWithNoPort() { - $loop = Factory::create(); try { - $socket = new SocketServer('127.0.0.1:80', array(), $loop); + $socket = new SocketServer('127.0.0.1:80'); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 80 failed (root and unused?)'); } - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -256,7 +246,7 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://127.0.0.1/', $response); @@ -266,15 +256,14 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort() { - $loop = Factory::create(); try { - $socket = new SocketServer('127.0.0.1:80', array(), $loop); + $socket = new SocketServer('127.0.0.1:80'); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 80 failed (root and unused?)'); } - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -286,7 +275,7 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://127.0.0.1/', $response); @@ -300,20 +289,19 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() $this->markTestSkipped('Not supported on HHVM'); } - $loop = Factory::create(); try { $socket = new SocketServer('tls://127.0.0.1:443', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )), $loop); + ))); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 443 failed (root and unused?)'); } $connector = new Connector(array( 'tls' => array('verify_peer' => false) - ), $loop); + )); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -325,7 +313,7 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://127.0.0.1/', $response); @@ -339,20 +327,19 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() $this->markTestSkipped('Not supported on HHVM'); } - $loop = Factory::create(); try { $socket = new SocketServer('tls://127.0.0.1:443', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )), $loop); + ))); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 443 failed (root and unused?)'); } $connector = new Connector(array( 'tls' => array('verify_peer' => false) - ), $loop); + )); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -364,7 +351,7 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://127.0.0.1/', $response); @@ -374,15 +361,14 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() { - $loop = Factory::create(); try { - $socket = new SocketServer('127.0.0.1:443', array(), $loop); + $socket = new SocketServer('127.0.0.1:443'); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 443 failed (root and unused?)'); } - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri()); }); @@ -394,7 +380,7 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://127.0.0.1:443/', $response); @@ -408,20 +394,19 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() $this->markTestSkipped('Not supported on HHVM'); } - $loop = Factory::create(); try { $socket = new SocketServer('tls://127.0.0.1:80', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' - )), $loop); + ))); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 80 failed (root and unused?)'); } $connector = new Connector(array( 'tls' => array('verify_peer' => false) - ), $loop); + )); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { return new Response(200, array(), (string)$request->getUri() . 'x' . $request->getHeaderLine('Host')); }); @@ -433,7 +418,7 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://127.0.0.1:80/', $response); @@ -443,17 +428,16 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() public function testClosedStreamFromRequestHandlerWillSendEmptyBody() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); $stream = new ThroughStream(); $stream->close(); - $http = new HttpServer($loop, function (RequestInterface $request) use ($stream) { + $http = new HttpServer(function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -462,7 +446,7 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertStringStartsWith("HTTP/1.0 200 OK", $response); $this->assertStringEndsWith("\r\n\r\n", $response); @@ -472,62 +456,58 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() public function testRequestHandlerWithStreamingRequestWillReceiveCloseEventIfConnectionClosesWhileSendingBody() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); $once = $this->expectCallableOnce(); $http = new HttpServer( - $loop, new StreamingRequestMiddleware(), function (RequestInterface $request) use ($once) { $request->getBody()->on('close', $once); } ); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); - $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { + $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nContent-Length: 100\r\n\r\n"); - $loop->addTimer(0.001, function() use ($conn) { + Loop::addTimer(0.001, function() use ($conn) { $conn->end(); }); }); - Block\sleep(0.1, $loop); + Block\sleep(0.1); $socket->close(); } public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileSendingStreamingRequestBody() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); $stream = new ThroughStream(); $http = new HttpServer( - $loop, new StreamingRequestMiddleware(), function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); } ); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); - $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { + $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nContent-Length: 100\r\n\r\n"); - $loop->addTimer(0.001, function() use ($conn) { + Loop::addTimer(0.001, function() use ($conn) { $conn->end(); }); }); // stream will be closed within 0.1s - $ret = Block\await(Stream\first($stream, 'close'), $loop, 0.1); + $ret = Block\await(Stream\first($stream, 'close'), null, 0.1); $socket->close(); @@ -536,28 +516,27 @@ function (RequestInterface $request) use ($stream) { public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); $stream = new ThroughStream(); - $http = new HttpServer($loop, function (RequestInterface $request) use ($stream) { + $http = new HttpServer(function (RequestInterface $request) use ($stream) { return new Response(200, array(), $stream); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); - $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) use ($loop) { + $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - $loop->addTimer(0.1, function () use ($conn) { + Loop::addTimer(0.1, function () use ($conn) { $conn->close(); }); }); // await response stream to be closed - $ret = Block\await(Stream\first($stream, 'close'), $loop, 1.0); + $ret = Block\await(Stream\first($stream, 'close'), null, 1.0); $socket->close(); @@ -566,20 +545,19 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() public function testUpgradeWithThroughStreamReturnsDataAsGiven() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { + $http = new HttpServer(function (RequestInterface $request) { $stream = new ThroughStream(); - $loop->addTimer(0.1, function () use ($stream) { + Loop::addTimer(0.1, function () use ($stream) { $stream->end(); }); return new Response(101, array('Upgrade' => 'echo'), $stream); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -593,7 +571,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertStringStartsWith("HTTP/1.1 101 Switching Protocols\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -603,20 +581,19 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { + $http = new HttpServer(function (RequestInterface $request) { $stream = new ThroughStream(); - $loop->addTimer(0.1, function () use ($stream) { + Loop::addTimer(0.1, function () use ($stream) { $stream->end(); }); return new Response(101, array('Upgrade' => 'echo'), $stream); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -631,7 +608,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertStringStartsWith("HTTP/1.1 101 Switching Protocols\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -641,20 +618,19 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() public function testConnectWithThroughStreamReturnsDataAsGiven() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { + $http = new HttpServer(function (RequestInterface $request) { $stream = new ThroughStream(); - $loop->addTimer(0.1, function () use ($stream) { + Loop::addTimer(0.1, function () use ($stream) { $stream->end(); }); return new Response(200, array(), $stream); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -668,7 +644,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -678,24 +654,23 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGiven() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) use ($loop) { + $http = new HttpServer(function (RequestInterface $request) { $stream = new ThroughStream(); - $loop->addTimer(0.1, function () use ($stream) { + Loop::addTimer(0.1, function () use ($stream) { $stream->end(); }); - return new Promise\Promise(function ($resolve) use ($loop, $stream) { - $loop->addTimer(0.001, function () use ($resolve, $stream) { + return new Promise\Promise(function ($resolve) use ($stream) { + Loop::addTimer(0.001, function () use ($resolve, $stream) { $resolve(new Response(200, array(), $stream)); }); }); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -709,7 +684,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -719,17 +694,16 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive public function testConnectWithClosedThroughStreamReturnsNoData() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); - $http = new HttpServer($loop, function (RequestInterface $request) { + $http = new HttpServer(function (RequestInterface $request) { $stream = new ThroughStream(); $stream->close(); return new Response(200, array(), $stream); }); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { @@ -743,7 +717,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() return Stream\buffer($conn); }); - $response = Block\await($result, $loop, 1.0); + $response = Block\await($result, null, 1.0); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); $this->assertStringEndsWith("\r\n\r\n", $response); @@ -753,16 +727,14 @@ public function testConnectWithClosedThroughStreamReturnsNoData() public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing() { - $loop = Factory::create(); - $connector = new Connector(array(), $loop); + $connector = new Connector(); $http = new HttpServer( - $loop, new LimitConcurrentRequestsMiddleware(5), new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB - function (ServerRequestInterface $request, $next) use ($loop) { - return new Promise\Promise(function ($resolve) use ($request, $loop, $next) { - $loop->addTimer(0.1, function () use ($request, $resolve, $next) { + function (ServerRequestInterface $request, $next) { + return new Promise\Promise(function ($resolve) use ($request, $next) { + Loop::addTimer(0.1, function () use ($request, $resolve, $next) { $resolve($next($request)); }); }); @@ -772,7 +744,7 @@ function (ServerRequestInterface $request) { } ); - $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); $result = array(); @@ -788,7 +760,7 @@ function (ServerRequestInterface $request) { }); } - $responses = Block\await(Promise\all($result), $loop, 1.0); + $responses = Block\await(Promise\all($result), null, 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 9200aa66..23dcc758 100644 --- a/tests/HttpServerTest.php +++ b/tests/HttpServerTest.php @@ -4,7 +4,7 @@ use Clue\React\Block; use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; +use React\EventLoop\Loop; use React\Http\HttpServer; use React\Http\Io\IniUtil; use React\Http\Middleware\StreamingRequestMiddleware; @@ -126,9 +126,8 @@ function (ServerRequestInterface $request) use (&$called) { public function testPostFormData() { - $loop = Factory::create(); $deferred = new Deferred(); - $http = new HttpServer($loop, function (ServerRequestInterface $request) use ($deferred) { + $http = new HttpServer(function (ServerRequestInterface $request) use ($deferred) { $deferred->resolve($request); }); @@ -136,7 +135,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(), $loop); + $request = Block\await($deferred->promise()); assert($request instanceof ServerRequestInterface); $form = $request->getParsedBody(); @@ -155,9 +154,8 @@ public function testPostFormData() public function testPostFileUpload() { - $loop = Factory::create(); $deferred = new Deferred(); - $http = new HttpServer($loop, function (ServerRequestInterface $request) use ($deferred) { + $http = new HttpServer(function (ServerRequestInterface $request) use ($deferred) { $deferred->resolve($request); }); @@ -166,16 +164,16 @@ public function testPostFileUpload() $connection = $this->connection; $data = $this->createPostFileUploadRequest(); - $loop->addPeriodicTimer(0.01, function ($timer) use ($loop, &$data, $connection) { + Loop::addPeriodicTimer(0.01, function ($timer) use (&$data, $connection) { $line = array_shift($data); $connection->emit('data', array($line)); if (count($data) === 0) { - $loop->cancelTimer($timer); + Loop::cancelTimer($timer); } }); - $request = Block\await($deferred->promise(), $loop); + $request = Block\await($deferred->promise()); assert($request instanceof ServerRequestInterface); $this->assertEmpty($request->getParsedBody()); @@ -199,9 +197,8 @@ public function testPostFileUpload() public function testPostJsonWillNotBeParsedByDefault() { - $loop = Factory::create(); $deferred = new Deferred(); - $http = new HttpServer($loop, function (ServerRequestInterface $request) use ($deferred) { + $http = new HttpServer(function (ServerRequestInterface $request) use ($deferred) { $deferred->resolve($request); }); @@ -209,7 +206,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(), $loop); + $request = Block\await($deferred->promise()); assert($request instanceof ServerRequestInterface); $this->assertNull($request->getParsedBody()); diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index eda61012..d8f5f232 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -6,7 +6,6 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; use React\Http\Io\MiddlewareRunner; use React\Http\Message\ServerRequest; use React\Promise; @@ -162,7 +161,7 @@ public function testProcessStack(array $middlewares, $expectedCallCount) $response = $middlewareStack($request); $this->assertTrue($response instanceof PromiseInterface); - $response = Block\await($response, Factory::create()); + $response = Block\await($response); $this->assertTrue($response instanceof ResponseInterface); $this->assertSame(200, $response->getStatusCode()); @@ -229,7 +228,7 @@ function () use ($errorHandler, &$called, $response, $exception) { $request = new ServerRequest('GET', 'https://example.com/'); - $this->assertSame($response, Block\await($runner($request), Factory::create())); + $this->assertSame($response, Block\await($runner($request))); $this->assertSame(1, $retryCalled); $this->assertSame(2, $called); $this->assertSame($exception, $error); diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index ccafe338..59cfd118 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -3,7 +3,7 @@ namespace React\Tests\Http\Io; use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; +use React\EventLoop\Loop; use React\Http\Io\StreamingServer; use React\Http\Message\Response; use React\Http\Message\ServerRequest; @@ -48,7 +48,7 @@ public function setUpConnectionMockAndSocket() public function testRequestEventWillNotBeEmittedForIncompleteHeaders() { - $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -60,7 +60,7 @@ public function testRequestEventWillNotBeEmittedForIncompleteHeaders() public function testRequestEventIsEmitted() { - $server = new StreamingServer(Factory::create(), $this->expectCallableOnce()); + $server = new StreamingServer(Loop::get(), $this->expectCallableOnce()); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -75,7 +75,7 @@ public function testRequestEventIsEmitted() public function testRequestEventIsEmittedForArrayCallable() { $this->called = null; - $server = new StreamingServer(Factory::create(), array($this, 'helperCallableOnce')); + $server = new StreamingServer(Loop::get(), array($this, 'helperCallableOnce')); $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); @@ -95,7 +95,7 @@ public function testRequestEvent() { $i = 0; $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; }); @@ -128,7 +128,7 @@ public function testRequestEventWithSingleRequestHandlerArray() { $i = 0; $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$i, &$requestAssertion) { $i++; $requestAssertion = $request; }); @@ -160,7 +160,7 @@ public function testRequestEventWithSingleRequestHandlerArray() public function testRequestGetWithHostAndCustomPort() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -182,7 +182,7 @@ public function testRequestGetWithHostAndCustomPort() public function testRequestGetWithHostAndHttpsPort() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -204,7 +204,7 @@ public function testRequestGetWithHostAndHttpsPort() public function testRequestGetWithHostAndDefaultPortWillBeIgnored() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -226,7 +226,7 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored() public function testRequestGetHttp10WithoutHostWillBeIgnored() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -248,7 +248,7 @@ public function testRequestGetHttp10WithoutHostWillBeIgnored() public function testRequestGetHttp11WithoutHostWillReject() { - $server = new StreamingServer(Factory::create(), 'var_dump'); + $server = new StreamingServer(Loop::get(), 'var_dump'); $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); @@ -261,7 +261,7 @@ public function testRequestGetHttp11WithoutHostWillReject() public function testRequestOptionsAsterisk() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -281,7 +281,7 @@ public function testRequestOptionsAsterisk() public function testRequestNonOptionsWithAsteriskRequestTargetWillReject() { - $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); @@ -294,7 +294,7 @@ public function testRequestNonOptionsWithAsteriskRequestTargetWillReject() public function testRequestConnectAuthorityForm() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -316,7 +316,7 @@ public function testRequestConnectAuthorityForm() public function testRequestConnectWithoutHostWillBePassesAsIs() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -338,7 +338,7 @@ public function testRequestConnectWithoutHostWillBePassesAsIs() public function testRequestConnectAuthorityFormWithDefaultPortWillBePassedAsIs() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -360,7 +360,7 @@ public function testRequestConnectAuthorityFormWithDefaultPortWillBePassedAsIs() public function testRequestConnectAuthorityFormNonMatchingHostWillBePassedAsIs() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -381,7 +381,7 @@ public function testRequestConnectAuthorityFormNonMatchingHostWillBePassedAsIs() public function testRequestConnectOriginFormRequestTargetWillReject() { - $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); @@ -393,7 +393,7 @@ public function testRequestConnectOriginFormRequestTargetWillReject() public function testRequestNonConnectWithAuthorityRequestTargetWillReject() { - $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); @@ -407,7 +407,7 @@ public function testRequestWithoutHostEventUsesSocketAddress() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -433,7 +433,7 @@ public function testRequestAbsoluteEvent() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -455,7 +455,7 @@ public function testRequestAbsoluteNonMatchingHostWillBePassedAsIs() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -475,7 +475,7 @@ public function testRequestAbsoluteNonMatchingHostWillBePassedAsIs() public function testRequestAbsoluteWithoutHostWillReject() { - $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); @@ -489,7 +489,7 @@ public function testRequestOptionsAsteriskEvent() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -511,7 +511,7 @@ public function testRequestOptionsAbsoluteEvent() { $requestAssertion = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestAssertion) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestAssertion) { $requestAssertion = $request; }); @@ -531,7 +531,7 @@ public function testRequestOptionsAbsoluteEvent() public function testRequestPauseWillBeForwardedToConnection() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { $request->getBody()->pause(); }); @@ -551,7 +551,7 @@ public function testRequestPauseWillBeForwardedToConnection() public function testRequestResumeWillBeForwardedToConnection() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { $request->getBody()->resume(); }); @@ -571,7 +571,7 @@ public function testRequestResumeWillBeForwardedToConnection() public function testRequestCloseWillNotCloseConnection() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { $request->getBody()->close(); }); @@ -586,7 +586,7 @@ public function testRequestCloseWillNotCloseConnection() public function testRequestPauseAfterCloseWillNotBeForwarded() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { $request->getBody()->close(); $request->getBody()->pause(); }); @@ -603,7 +603,7 @@ public function testRequestPauseAfterCloseWillNotBeForwarded() public function testRequestResumeAfterCloseWillNotBeForwarded() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { $request->getBody()->close(); $request->getBody()->resume(); }); @@ -622,7 +622,7 @@ public function testRequestEventWithoutBodyWillNotEmitData() { $never = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($never) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($never) { $request->getBody()->on('data', $never); }); @@ -637,7 +637,7 @@ public function testRequestEventWithSecondDataEventWillEmitBodyData() { $once = $this->expectCallableOnceWith('incomplete'); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($once) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($once) { $request->getBody()->on('data', $once); }); @@ -657,7 +657,7 @@ public function testRequestEventWithPartialBodyWillEmitData() { $once = $this->expectCallableOnceWith('incomplete'); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($once) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($once) { $request->getBody()->on('data', $once); }); @@ -678,7 +678,7 @@ public function testRequestEventWithPartialBodyWillEmitData() public function testResponseContainsServerHeader() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response(); }); @@ -708,7 +708,7 @@ public function testResponsePendingPromiseWillNotSendAnything() { $never = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($never) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($never) { return new Promise(function () { }, $never); }); @@ -738,7 +738,7 @@ public function testResponsePendingPromiseWillBeCancelledIfConnectionCloses() { $once = $this->expectCallableOnce(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($once) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($once) { return new Promise(function () { }, $once); }); @@ -770,7 +770,7 @@ public function testResponseBodyStreamAlreadyClosedWillSendEmptyBodyChunkedEncod $stream = new ThroughStream(); $stream->close(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -805,7 +805,7 @@ public function testResponseBodyStreamEndingWillSendEmptyBodyChunkedEncoded() { $stream = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -843,7 +843,7 @@ public function testResponseBodyStreamAlreadyClosedWillSendEmptyBodyPlainHttp10( $stream = new ThroughStream(); $stream->close(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -879,7 +879,7 @@ public function testResponseStreamWillBeClosedIfConnectionIsAlreadyClosed() $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -934,7 +934,7 @@ public function testResponseBodyStreamWillBeClosedIfConnectionEmitsCloseEvent() $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -952,7 +952,7 @@ public function testResponseBodyStreamWillBeClosedIfConnectionEmitsCloseEvent() public function testResponseUpgradeInResponseCanBeUsedToAdvertisePossibleUpgrade() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array( @@ -988,7 +988,7 @@ function ($data) use (&$buffer) { public function testResponseUpgradeWishInRequestCanBeIgnoredByReturningNormalResponse() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array( @@ -1023,7 +1023,7 @@ function ($data) use (&$buffer) { public function testResponseUpgradeSwitchingProtocolIncludesConnectionUpgradeHeaderWithoutContentLength() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 101, array( @@ -1063,7 +1063,7 @@ public function testResponseUpgradeSwitchingProtocolWithStreamWillPipeDataToConn { $stream = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 101, array( @@ -1104,7 +1104,7 @@ public function testResponseConnectMethodStreamWillPipeDataToConnection() { $stream = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -1142,7 +1142,7 @@ public function testResponseConnectMethodStreamWillPipeDataFromConnection() { $stream = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -1161,7 +1161,7 @@ public function testResponseConnectMethodStreamWillPipeDataFromConnection() public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -1194,7 +1194,7 @@ function ($data) use (&$buffer) { public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp10() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -1228,7 +1228,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyForHeadRequest() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array(), @@ -1264,7 +1264,7 @@ public function testResponseContainsNoResponseBodyForHeadRequestWithStreamingRes $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array('Content-Length' => '3'), @@ -1296,7 +1296,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContentStatus() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 204, array(), @@ -1332,7 +1332,7 @@ public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContent $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 204, array('Content-Length' => '3'), @@ -1364,7 +1364,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoContentLengthHeaderForNotModifiedStatus() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 304, array(), @@ -1396,7 +1396,7 @@ function ($data) use (&$buffer) { public function testResponseContainsExplicitContentLengthHeaderForNotModifiedStatus() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 304, array('Content-Length' => 3), @@ -1428,7 +1428,7 @@ function ($data) use (&$buffer) { public function testResponseContainsNoResponseBodyForNotModifiedStatus() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 304, array(), @@ -1464,7 +1464,7 @@ public function testResponseContainsNoResponseBodyForNotModifiedStatusWithStream $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 304, array('Content-Length' => '3'), @@ -1497,7 +1497,7 @@ function ($data) use (&$buffer) { public function testRequestInvalidHttpProtocolVersionWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1531,7 +1531,7 @@ function ($data) use (&$buffer) { public function testRequestOverflowWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1565,7 +1565,7 @@ function ($data) use (&$buffer) { public function testRequestInvalidWillEmitErrorAndSendErrorResponse() { $error = null; - $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $server->on('error', function ($message) use (&$error) { $error = $message; }); @@ -1602,7 +1602,7 @@ public function testRequestContentLengthBodyDataWillEmitDataEventOnRequestStream $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1630,7 +1630,7 @@ public function testRequestChunkedTransferEncodingRequestWillEmitDecodedDataEven $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1661,7 +1661,7 @@ public function testRequestChunkedTransferEncodingWithAdditionalDataWontBeEmitte $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1690,7 +1690,7 @@ public function testRequestChunkedTransferEncodingEmpty() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1718,7 +1718,7 @@ public function testRequestChunkedTransferEncodingHeaderCanBeUpperCase() $errorEvent = $this->expectCallableNever(); $requestValidation = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent, &$requestValidation) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1748,7 +1748,7 @@ public function testRequestChunkedTransferEncodingCanBeMixedUpperAndLowerCase() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1775,7 +1775,7 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1806,7 +1806,7 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1838,7 +1838,7 @@ public function testRequestZeroContentLengthWillEmitEndEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1864,7 +1864,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1891,7 +1891,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -1917,7 +1917,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ + $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); return \React\Promise\resolve(new Response()); }); @@ -1942,7 +1942,7 @@ public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ + $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -1964,7 +1964,7 @@ public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ + $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -1987,7 +1987,7 @@ public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWi public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ + $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -2009,7 +2009,7 @@ public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() public function testRequestUnexpectedEndOfRequestWithContentLengthWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); - $server = new StreamingServer(Factory::create(), function ($request) use ($errorEvent){ + $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -2036,7 +2036,7 @@ public function testRequestWithoutBodyWillEmitEndOnRequestStream() $endEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function ($request) use ($dataEvent, $closeEvent, $endEvent, $errorEvent){ + $server = new StreamingServer(Loop::get(), function ($request) use ($dataEvent, $closeEvent, $endEvent, $errorEvent){ $request->getBody()->on('data', $dataEvent); $request->getBody()->on('close', $closeEvent); $request->getBody()->on('end', $endEvent); @@ -2060,7 +2060,7 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() $closeEvent = $this->expectCallableOnce(); $errorEvent = $this->expectCallableNever(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($dataEvent, $endEvent, $closeEvent, $errorEvent) { $request->getBody()->on('data', $dataEvent); $request->getBody()->on('end', $endEvent); $request->getBody()->on('close', $closeEvent); @@ -2079,7 +2079,7 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() public function testResponseWithBodyStreamWillUseChunkedTransferEncodingByDefault() { $stream = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array(), @@ -2113,7 +2113,7 @@ function ($data) use (&$buffer) { public function testResponseWithBodyStringWillOverwriteExplicitContentLengthAndTransferEncoding() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array( @@ -2154,7 +2154,7 @@ public function testResponseContainsResponseBodyWithTransferEncodingChunkedForBo $body->expects($this->once())->method('getSize')->willReturn(null); $body->expects($this->once())->method('__toString')->willReturn('body'); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($body) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($body) { return new Response( 200, array(), @@ -2191,7 +2191,7 @@ public function testResponseContainsResponseBodyWithPlainBodyWithUnknownSizeForL $body->expects($this->once())->method('getSize')->willReturn(null); $body->expects($this->once())->method('__toString')->willReturn('body'); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($body) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($body) { return new Response( 200, array(), @@ -2225,7 +2225,7 @@ function ($data) use (&$buffer) { public function testResponseWithCustomTransferEncodingWillBeIgnoredAndUseChunkedTransferEncodingInstead() { $stream = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($stream) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, array( @@ -2262,7 +2262,7 @@ function ($data) use (&$buffer) { public function testResponseWithoutExplicitDateHeaderWillAddCurrentDate() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response(); }); @@ -2292,7 +2292,7 @@ function ($data) use (&$buffer) { public function testResponseWIthCustomDateHeaderOverwritesDefault() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array("Date" => "Tue, 15 Nov 1994 08:12:31 GMT") @@ -2325,7 +2325,7 @@ function ($data) use (&$buffer) { public function testResponseWithEmptyDateHeaderRemovesDateHeader() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array('Date' => '') @@ -2358,7 +2358,7 @@ function ($data) use (&$buffer) { public function testResponseCanContainMultipleCookieHeaders() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array( @@ -2396,7 +2396,7 @@ function ($data) use (&$buffer) { public function testReponseWithExpectContinueRequestContainsContinueWithLaterResponse() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response(); }); @@ -2428,7 +2428,7 @@ function ($data) use (&$buffer) { public function testResponseWithExpectContinueRequestWontSendContinueForHttp10() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response(); }); @@ -2459,14 +2459,14 @@ function ($data) use (&$buffer) { public function testInvalidCallbackFunctionLeadsToException() { $this->setExpectedException('InvalidArgumentException'); - $server = new StreamingServer(Factory::create(), 'invalid'); + $server = new StreamingServer(Loop::get(), 'invalid'); } public function testResponseBodyStreamWillStreamDataWithChunkedTransferEncoding() { $input = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($input) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($input) { return new Response( 200, array(), @@ -2505,7 +2505,7 @@ public function testResponseBodyStreamWithContentLengthWillStreamTillLengthWitho { $input = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use ($input) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($input) { return new Response( 200, array('Content-Length' => 5), @@ -2543,7 +2543,7 @@ function ($data) use (&$buffer) { public function testResponseWithResponsePromise() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return \React\Promise\resolve(new Response()); }); @@ -2571,7 +2571,7 @@ function ($data) use (&$buffer) { public function testResponseReturnInvalidTypeWillResultInError() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return "invalid"; }); @@ -2605,7 +2605,7 @@ function ($data) use (&$buffer) { public function testResponseResolveWrongTypeInPromiseWillResultInError() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return \React\Promise\resolve("invalid"); }); @@ -2633,7 +2633,7 @@ function ($data) use (&$buffer) { public function testResponseRejectedPromiseWillResultInErrorMessage() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { $reject(new \Exception()); }); @@ -2664,7 +2664,7 @@ function ($data) use (&$buffer) { public function testResponseExceptionInCallbackWillResultInErrorMessage() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { throw new \Exception('Bad call'); }); @@ -2695,7 +2695,7 @@ function ($data) use (&$buffer) { public function testResponseWithContentLengthHeaderForStringBodyOverwritesTransferEncoding() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, array('Transfer-Encoding' => 'chunked'), @@ -2731,7 +2731,7 @@ function ($data) use (&$buffer) { public function testResponseWillBeHandled() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response(); }); @@ -2759,7 +2759,7 @@ function ($data) use (&$buffer) { public function testResponseExceptionThrowInCallBackFunctionWillResultInErrorMessage() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { throw new \Exception('hello'); }); @@ -2797,7 +2797,7 @@ function ($data) use (&$buffer) { */ public function testResponseThrowableThrowInCallBackFunctionWillResultInErrorMessage() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { throw new \Error('hello'); }); @@ -2840,7 +2840,7 @@ function ($data) use (&$buffer) { public function testResponseRejectOfNonExceptionWillResultInErrorMessage() { - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Promise(function ($resolve, $reject) { $reject('Invalid type'); }); @@ -2877,7 +2877,7 @@ function ($data) use (&$buffer) { public function testRequestServerRequestParams() { $requestValidation = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2911,7 +2911,7 @@ public function testRequestServerRequestParams() public function testRequestQueryParametersWillBeAddedToRequest() { $requestValidation = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2931,7 +2931,7 @@ public function testRequestQueryParametersWillBeAddedToRequest() public function testRequestCookieWillBeAddedToServerRequest() { $requestValidation = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2952,7 +2952,7 @@ public function testRequestCookieWillBeAddedToServerRequest() public function testRequestInvalidMultipleCookiesWontBeAddedToServerRequest() { $requestValidation = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2973,7 +2973,7 @@ public function testRequestInvalidMultipleCookiesWontBeAddedToServerRequest() public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() { $requestValidation = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -2992,7 +2992,7 @@ public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() { $requestValidation = null; - $server = new StreamingServer(Factory::create(), function (ServerRequestInterface $request) use (&$requestValidation) { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use (&$requestValidation) { $requestValidation = $request; }); @@ -3011,7 +3011,7 @@ public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() { public function testNewConnectionWillInvokeParserOnce() { - $server = new StreamingServer(Factory::create(), $this->expectCallableNever()); + $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); $parser->expects($this->once())->method('handle'); @@ -3028,7 +3028,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen { $request = new ServerRequest('GET', 'http://localhost/', array(), '', '1.0'); - $server = new StreamingServer(Factory::create(), $this->expectCallableOnceWith($request)); + $server = new StreamingServer(Loop::get(), $this->expectCallableOnceWith($request)); $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); $parser->expects($this->once())->method('handle'); @@ -3051,7 +3051,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen { $request = new ServerRequest('GET', 'http://localhost/', array('Connection' => 'close')); - $server = new StreamingServer(Factory::create(), $this->expectCallableOnceWith($request)); + $server = new StreamingServer(Loop::get(), $this->expectCallableOnceWith($request)); $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->getMock(); $parser->expects($this->once())->method('handle'); @@ -3074,7 +3074,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen { $request = new ServerRequest('GET', 'http://localhost/'); - $server = new StreamingServer(Factory::create(), function () { + $server = new StreamingServer(Loop::get(), function () { return new Response(200, array('Connection' => 'close')); }); @@ -3099,7 +3099,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle { $request = new ServerRequest('GET', 'http://localhost/'); - $server = new StreamingServer(Factory::create(), function () { + $server = new StreamingServer(Loop::get(), function () { return new Response(); }); @@ -3124,7 +3124,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle { $request = new ServerRequest('GET', 'http://localhost/', array('Connection' => 'keep-alive'), '', '1.0'); - $server = new StreamingServer(Factory::create(), function () { + $server = new StreamingServer(Loop::get(), function () { return new Response(); }); @@ -3150,7 +3150,7 @@ public function testNewConnectionWillInvokeParserOnceAfterInvokingRequestHandler $request = new ServerRequest('GET', 'http://localhost/'); $body = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function () use ($body) { + $server = new StreamingServer(Loop::get(), function () use ($body) { return new Response(200, array(), $body); }); @@ -3176,7 +3176,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle $request = new ServerRequest('GET', 'http://localhost/'); $body = new ThroughStream(); - $server = new StreamingServer(Factory::create(), function () use ($body) { + $server = new StreamingServer(Loop::get(), function () use ($body) { return new Response(200, array(), $body); }); diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 12e128cc..d62147b5 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -8,7 +8,7 @@ use RingCentral\Psr7\Response; use React\Http\Io\Transaction; use React\Http\Message\ResponseException; -use React\EventLoop\Factory; +use React\EventLoop\Loop; use React\Promise; use React\Promise\Deferred; use React\Stream\ThroughStream; @@ -383,10 +383,8 @@ public function testReceivingErrorResponseWillRejectWithResponseException() public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefault() { - $loop = Factory::create(); - $stream = new ThroughStream(); - $loop->addTimer(0.001, function () use ($stream) { + Loop::addTimer(0.001, function () use ($stream) { $stream->emit('data', array('hello world')); $stream->close(); }); @@ -398,10 +396,10 @@ public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefau $sender = $this->makeSenderMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); - $transaction = new Transaction($sender, $loop); + $transaction = new Transaction($sender, Loop::get()); $promise = $transaction->send($request); - $response = Block\await($promise, $loop); + $response = Block\await($promise); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('hello world', (string)$response->getBody()); @@ -409,8 +407,6 @@ public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefau public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBufferWillRejectAndCloseResponseStream() { - $loop = Factory::create(); - $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); @@ -422,17 +418,15 @@ public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBuffer $sender = $this->makeSenderMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); - $transaction = new Transaction($sender, $loop); + $transaction = new Transaction($sender, Loop::get()); $promise = $transaction->send($request); $this->setExpectedException('OverflowException'); - Block\await($promise, $loop, 0.001); + Block\await($promise, null, 0.001); } public function testCancelBufferingResponseWillCloseStreamAndReject() { - $loop = Factory::create(); - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); $stream->expects($this->any())->method('isReadable')->willReturn(true); $stream->expects($this->once())->method('close'); @@ -444,12 +438,12 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() $sender = $this->makeSenderMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\resolve($response)); - $transaction = new Transaction($sender, $loop); + $transaction = new Transaction($sender, Loop::get()); $promise = $transaction->send($request); $promise->cancel(); $this->setExpectedException('RuntimeException'); - Block\await($promise, $loop, 0.001); + Block\await($promise, null, 0.001); } public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStreamingIsEnabled() diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index 9889f501..e073e1f0 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -4,7 +4,7 @@ use Clue\React\Block; use Psr\Http\Message\ServerRequestInterface; -use React\EventLoop\Factory; +use React\EventLoop\Loop; use React\Http\Io\HttpBodyStream; use React\Http\Message\Response; use React\Http\Message\ServerRequest; @@ -118,8 +118,6 @@ function (ServerRequestInterface $request) use (&$exposedRequest) { public function testKnownExcessiveSizedBodyIsDisgardedTheRequestIsPassedDownToTheNextMiddleware() { - $loop = Factory::create(); - $stream = new ThroughStream(); $stream->end('aa'); $serverRequest = new ServerRequest( @@ -135,7 +133,7 @@ public function testKnownExcessiveSizedBodyIsDisgardedTheRequestIsPassedDownToTh function (ServerRequestInterface $request) { return new Response(200, array(), $request->getBody()->getContents()); } - ), $loop); + )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('', $response->getBody()->getContents()); @@ -143,10 +141,8 @@ function (ServerRequestInterface $request) { public function testKnownExcessiveSizedWithIniLikeSize() { - $loop = Factory::create(); - $stream = new ThroughStream(); - $loop->addTimer(0.001, function () use ($stream) { + Loop::addTimer(0.001, function () use ($stream) { $stream->end(str_repeat('a', 2048)); }); $serverRequest = new ServerRequest( @@ -162,7 +158,7 @@ public function testKnownExcessiveSizedWithIniLikeSize() function (ServerRequestInterface $request) { return new Response(200, array(), $request->getBody()->getContents()); } - ), $loop); + )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('', $response->getBody()->getContents()); @@ -192,8 +188,6 @@ function (ServerRequestInterface $request) use (&$exposedRequest) { public function testExcessiveSizeBodyIsDiscardedAndTheRequestIsPassedDownToTheNextMiddleware() { - $loop = Factory::create(); - $stream = new ThroughStream(); $serverRequest = new ServerRequest( 'GET', @@ -215,7 +209,7 @@ function (ServerRequestInterface $request) { $exposedResponse = Block\await($promise->then( null, $this->expectCallableNever() - ), $loop); + )); $this->assertSame(200, $exposedResponse->getStatusCode()); $this->assertSame('', $exposedResponse->getBody()->getContents()); @@ -223,8 +217,6 @@ function (ServerRequestInterface $request) { public function testBufferingErrorThrows() { - $loop = Factory::create(); - $stream = new ThroughStream(); $serverRequest = new ServerRequest( 'GET', @@ -244,7 +236,7 @@ function (ServerRequestInterface $request) { $stream->emit('error', array(new \RuntimeException())); $this->setExpectedException('RuntimeException'); - Block\await($promise, $loop); + Block\await($promise); } public function testFullBodyStreamedBeforeCallingNextMiddleware() From 6ca832f446a8467b4c730101f5df2200291a64b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 7 Apr 2021 17:49:31 +0200 Subject: [PATCH 084/152] Add factory methods for common HTML/JSON/plaintext/XML response types --- README.md | 181 +++++++++++++++++++++++++++ src/Message/Response.php | 215 +++++++++++++++++++++++++++++++++ tests/Message/ResponseTest.php | 71 +++++++++++ 3 files changed, 467 insertions(+) diff --git a/README.md b/README.md index 50797fe6..e178ee72 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,10 @@ multiple concurrent HTTP requests without blocking. * [withResponseBuffer()](#withresponsebuffer) * [React\Http\Message](#reacthttpmessage) * [Response](#response) + * [html()](#html) + * [json()](#json) + * [plaintext()](#plaintext) + * [xml()](#xml) * [ServerRequest](#serverrequest) * [ResponseException](#responseexception) * [React\Http\Middleware](#reacthttpmiddleware) @@ -2463,6 +2467,183 @@ constants with the `STATUS_*` prefix. For instance, the `200 OK` and response message and only adds required streaming support. This base class is considered an implementation detail that may change in the future. +##### html() + +The static `html(string $html): Response` method can be used to +create an HTML response. + +```php +$html = << + +Hello wörld! + + +HTML; + +$response = React\Http\Message\Response::html($html); +``` + +This is a convenient shortcut method that returns the equivalent of this: + +``` +$response = new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + [ + 'Content-Type' => 'text/html; charset=utf-8' + ], + $html +); +``` + +This method always returns a response with a `200 OK` status code and +the appropriate `Content-Type` response header for the given HTTP source +string encoded in UTF-8 (Unicode). It's generally recommended to end the +given plaintext string with a trailing newline. + +If you want to use a different status code or custom HTTP response +headers, you can manipulate the returned response object using the +provided PSR-7 methods or directly instantiate a custom HTTP response +object using the `Response` constructor: + +```php +$response = React\Http\Message\Response::html( + "

Error

\n

Invalid user name given.

\n" +)->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); +``` + +##### json() + +The static `json(mixed $data): Response` method can be used to +create a JSON response. + +```php +$response = React\Http\Message\Response::json(['name' => 'Alice']); +``` + +This is a convenient shortcut method that returns the equivalent of this: + +``` +$response = new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + [ + 'Content-Type' => 'application/json' + ], + json_encode( + ['name' => 'Alice'], + JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION + ) . "\n" +); +``` + +This method always returns a response with a `200 OK` status code and +the appropriate `Content-Type` response header for the given structured +data encoded as a JSON text. + +The given structured data will be encoded as a JSON text. Any `string` +values in the data must be encoded in UTF-8 (Unicode). If the encoding +fails, this method will throw an `InvalidArgumentException`. + +By default, the given structured data will be encoded with the flags as +shown above. This includes pretty printing (PHP 5.4+) and preserving +zero fractions for `float` values (PHP 5.6.6+) to ease debugging. It is +assumed any additional data overhead is usually compensated by using HTTP +response compression. + +If you want to use a different status code or custom HTTP response +headers, you can manipulate the returned response object using the +provided PSR-7 methods or directly instantiate a custom HTTP response +object using the `Response` constructor: + +```php +$response = React\Http\Message\Response::json( + ['error' => 'Invalid user name given'] +)->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); +``` + +##### plaintext() + +The static `plaintext(string $text): Response` method can be used to +create a plaintext response. + +```php +$response = React\Http\Message\Response::plaintext("Hello wörld!\n"); +``` + +This is a convenient shortcut method that returns the equivalent of this: + +``` +$response = new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + [ + 'Content-Type' => 'text/plain; charset=utf-8' + ], + "Hello wörld!\n" +); +``` + +This method always returns a response with a `200 OK` status code and +the appropriate `Content-Type` response header for the given plaintext +string encoded in UTF-8 (Unicode). It's generally recommended to end the +given plaintext string with a trailing newline. + +If you want to use a different status code or custom HTTP response +headers, you can manipulate the returned response object using the +provided PSR-7 methods or directly instantiate a custom HTTP response +object using the `Response` constructor: + +```php +$response = React\Http\Message\Response::plaintext( + "Error: Invalid user name given.\n" +)->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); +``` + +##### xml() + +The static `xml(string $xml): Response` method can be used to +create an XML response. + +```php +$xml = << + + Hello wörld! + + +XML; + +$response = React\Http\Message\Response::xml($xml); +``` + +This is a convenient shortcut method that returns the equivalent of this: + +``` +$response = new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + [ + 'Content-Type' => 'application/xml' + ], + $xml +); +``` + +This method always returns a response with a `200 OK` status code and +the appropriate `Content-Type` response header for the given XML source +string. It's generally recommended to use UTF-8 (Unicode) and specify +this as part of the leading XML declaration and to end the given XML +source string with a trailing newline. + +If you want to use a different status code or custom HTTP response +headers, you can manipulate the returned response object using the +provided PSR-7 methods or directly instantiate a custom HTTP response +object using the `Response` constructor: + +```php +$response = React\Http\Message\Response::xml( + "Invalid user name given.\n" +)->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); +``` + #### ServerRequest The `React\Http\Message\ServerRequest` class can be used to diff --git a/src/Message/Response.php b/src/Message/Response.php index 0ce8bef0..edd6245b 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -42,6 +42,221 @@ */ final class Response extends Psr7Response implements StatusCodeInterface { + /** + * Create an HTML response + * + * ```php + * $html = << + * + * Hello wörld! + * + * + * HTML; + * + * $response = React\Http\Message\Response::html($html); + * ``` + * + * This is a convenient shortcut method that returns the equivalent of this: + * + * ``` + * $response = new React\Http\Message\Response( + * React\Http\Message\Response::STATUS_OK, + * [ + * 'Content-Type' => 'text/html; charset=utf-8' + * ], + * $html + * ); + * ``` + * + * This method always returns a response with a `200 OK` status code and + * the appropriate `Content-Type` response header for the given HTTP source + * string encoded in UTF-8 (Unicode). It's generally recommended to end the + * given plaintext string with a trailing newline. + * + * If you want to use a different status code or custom HTTP response + * headers, you can manipulate the returned response object using the + * provided PSR-7 methods or directly instantiate a custom HTTP response + * object using the `Response` constructor: + * + * ```php + * $response = React\Http\Message\Response::html( + * "

Error

\n

Invalid user name given.

\n" + * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); + * ``` + * + * @param string $html + * @return self + */ + public static function html($html) + { + return new self(self::STATUS_OK, array('Content-Type' => 'text/html; charset=utf-8'), $html); + } + + /** + * Create a JSON response + * + * ```php + * $response = React\Http\Message\Response::json(['name' => 'Alice']); + * ``` + * + * This is a convenient shortcut method that returns the equivalent of this: + * + * ``` + * $response = new React\Http\Message\Response( + * React\Http\Message\Response::STATUS_OK, + * [ + * 'Content-Type' => 'application/json' + * ], + * json_encode( + * ['name' => 'Alice'], + * JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION + * ) . "\n" + * ); + * ``` + * + * This method always returns a response with a `200 OK` status code and + * the appropriate `Content-Type` response header for the given structured + * data encoded as a JSON text. + * + * The given structured data will be encoded as a JSON text. Any `string` + * values in the data must be encoded in UTF-8 (Unicode). If the encoding + * fails, this method will throw an `InvalidArgumentException`. + * + * By default, the given structured data will be encoded with the flags as + * shown above. This includes pretty printing (PHP 5.4+) and preserving + * zero fractions for `float` values (PHP 5.6.6+) to ease debugging. It is + * assumed any additional data overhead is usually compensated by using HTTP + * response compression. + * + * If you want to use a different status code or custom HTTP response + * headers, you can manipulate the returned response object using the + * provided PSR-7 methods or directly instantiate a custom HTTP response + * object using the `Response` constructor: + * + * ```php + * $response = React\Http\Message\Response::json( + * ['error' => 'Invalid user name given'] + * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); + * ``` + * + * @param mixed $data + * @return self + * @throws \InvalidArgumentException when encoding fails + */ + public static function json($data) + { + $json = @\json_encode( + $data, + (\defined('JSON_PRETTY_PRINT') ? \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE : 0) | (\defined('JSON_PRESERVE_ZERO_FRACTION') ? \JSON_PRESERVE_ZERO_FRACTION : 0) + ); + + // throw on error, now `false` but used to be `(string) "null"` before PHP 5.5 + if ($json === false || (\PHP_VERSION_ID < 50500 && \json_last_error() !== \JSON_ERROR_NONE)) { + throw new \InvalidArgumentException( + 'Unable to encode given data as JSON' . (\function_exists('json_last_error_msg') ? ': ' . \json_last_error_msg() : ''), + \json_last_error() + ); + } + + return new self(self::STATUS_OK, array('Content-Type' => 'application/json'), $json . "\n"); + } + + /** + * Create a plaintext response + * + * ```php + * $response = React\Http\Message\Response::plaintext("Hello wörld!\n"); + * ``` + * + * This is a convenient shortcut method that returns the equivalent of this: + * + * ``` + * $response = new React\Http\Message\Response( + * React\Http\Message\Response::STATUS_OK, + * [ + * 'Content-Type' => 'text/plain; charset=utf-8' + * ], + * "Hello wörld!\n" + * ); + * ``` + * + * This method always returns a response with a `200 OK` status code and + * the appropriate `Content-Type` response header for the given plaintext + * string encoded in UTF-8 (Unicode). It's generally recommended to end the + * given plaintext string with a trailing newline. + * + * If you want to use a different status code or custom HTTP response + * headers, you can manipulate the returned response object using the + * provided PSR-7 methods or directly instantiate a custom HTTP response + * object using the `Response` constructor: + * + * ```php + * $response = React\Http\Message\Response::plaintext( + * "Error: Invalid user name given.\n" + * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); + * ``` + * + * @param string $text + * @return self + */ + public static function plaintext($text) + { + return new self(self::STATUS_OK, array('Content-Type' => 'text/plain; charset=utf-8'), $text); + } + + /** + * Create an XML response + * + * ```php + * $xml = << + * + * Hello wörld! + * + * + * XML; + * + * $response = React\Http\Message\Response::xml($xml); + * ``` + * + * This is a convenient shortcut method that returns the equivalent of this: + * + * ``` + * $response = new React\Http\Message\Response( + * React\Http\Message\Response::STATUS_OK, + * [ + * 'Content-Type' => 'application/xml' + * ], + * $xml + * ); + * ``` + * + * This method always returns a response with a `200 OK` status code and + * the appropriate `Content-Type` response header for the given XML source + * string. It's generally recommended to use UTF-8 (Unicode) and specify + * this as part of the leading XML declaration and to end the given XML + * source string with a trailing newline. + * + * If you want to use a different status code or custom HTTP response + * headers, you can manipulate the returned response object using the + * provided PSR-7 methods or directly instantiate a custom HTTP response + * object using the `Response` constructor: + * + * ```php + * $response = React\Http\Message\Response::xml( + * "Invalid user name given.\n" + * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); + * ``` + * + * @param string $xml + * @return self + */ + public static function xml($xml) + { + return new self(self::STATUS_OK, array('Content-Type' => 'application/xml'), $xml); + } + /** * @param int $status HTTP status code (e.g. 200/404), see `self::STATUS_*` constants * @param array $headers additional response headers diff --git a/tests/Message/ResponseTest.php b/tests/Message/ResponseTest.php index 3c6ad3fd..ed21cdc2 100644 --- a/tests/Message/ResponseTest.php +++ b/tests/Message/ResponseTest.php @@ -53,4 +53,75 @@ public function testResourceBodyWillThrow() $this->setExpectedException('InvalidArgumentException'); new Response(200, array(), tmpfile()); } + + + public function testHtmlMethodReturnsHtmlResponse() + { + $response = Response::html('Hello wörld!'); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); + $this->assertEquals('Hello wörld!', (string) $response->getBody()); + } + + /** + * @requires PHP 5.4 + */ + public function testJsonMethodReturnsPrettyPrintedJsonResponse() + { + $response = Response::json(array('text' => 'Hello wörld!')); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('application/json', $response->getHeaderLine('Content-Type')); + $this->assertEquals("{\n \"text\": \"Hello wörld!\"\n}\n", (string) $response->getBody()); + } + + /** + * @requires PHP 5.6.6 + */ + public function testJsonMethodReturnsZeroFractionsInJsonResponse() + { + $response = Response::json(1.0); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('application/json', $response->getHeaderLine('Content-Type')); + $this->assertEquals("1.0\n", (string) $response->getBody()); + } + + public function testJsonMethodReturnsJsonTextForSimpleString() + { + $response = Response::json('Hello world!'); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('application/json', $response->getHeaderLine('Content-Type')); + $this->assertEquals("\"Hello world!\"\n", (string) $response->getBody()); + } + + public function testJsonMethodThrowsForInvalidString() + { + if (PHP_VERSION_ID < 50500) { + $this->setExpectedException('InvalidArgumentException', 'Unable to encode given data as JSON'); + } else { + $this->setExpectedException('InvalidArgumentException', 'Unable to encode given data as JSON: Malformed UTF-8 characters, possibly incorrectly encoded'); + } + Response::json("Hello w\xF6rld!"); + } + + public function testPlaintextMethodReturnsPlaintextResponse() + { + $response = Response::plaintext("Hello wörld!\n"); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('text/plain; charset=utf-8', $response->getHeaderLine('Content-Type')); + $this->assertEquals("Hello wörld!\n", (string) $response->getBody()); + } + + public function testXmlMethodReturnsXmlResponse() + { + $response = Response::xml('Hello wörld!'); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('application/xml', $response->getHeaderLine('Content-Type')); + $this->assertEquals('Hello wörld!', (string) $response->getBody()); + } } From a49404c6e61dc01623844d887e3533e6a98c3542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 22 Jan 2022 14:31:58 +0100 Subject: [PATCH 085/152] Update documentation and examples to use new response factory methods --- README.md | 140 ++++++---------------- examples/51-server-hello-world.php | 13 +- examples/52-server-count-visitors.php | 11 +- examples/53-server-whatsmyip.php | 13 +- examples/54-server-query-parameter.php | 11 +- examples/55-server-cookie-handling.php | 24 +--- examples/56-server-sleep.php | 22 ++-- examples/57-server-error-handling.php | 28 ++--- examples/58-server-stream-response.php | 3 +- examples/59-server-json-api.php | 41 ++----- examples/61-server-hello-world-https.php | 13 +- examples/62-server-form-upload.php | 6 +- examples/63-server-streaming-request.php | 14 +-- examples/71-server-http-proxy.php | 24 +--- examples/99-server-benchmark-download.php | 6 +- 15 files changed, 99 insertions(+), 270 deletions(-) diff --git a/README.md b/README.md index e178ee72..23e4b5fa 100644 --- a/README.md +++ b/README.md @@ -112,11 +112,7 @@ This is an HTTP server which responds with `Hello World!` to every request. require __DIR__ . '/vendor/autoload.php'; $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( "Hello World!\n" ); }); @@ -738,11 +734,7 @@ object and expects a [response](#server-response) object in return: ```php $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( "Hello World!\n" ); }); @@ -953,14 +945,10 @@ and will be passed to the callback function like this. ```php $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { - $body = "The method of the request is: " . $request->getMethod(); - $body .= "The requested path is: " . $request->getUri()->getPath(); + $body = "The method of the request is: " . $request->getMethod() . "\n"; + $body .= "The requested path is: " . $request->getUri()->getPath() . "\n"; - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( $body ); }); @@ -996,13 +984,9 @@ The following parameters are currently available: ```php $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { - $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR']; + $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR'] . "\n"; - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( $body ); }); @@ -1030,11 +1014,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $body = 'The value of "foo" is: ' . htmlspecialchars($queryParams['foo']); } - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/html' - ), + return React\Http\Message\Response::html( $body ); }); @@ -1077,9 +1057,7 @@ request headers (commonly used for `POST` requests for HTML form submission data $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { $name = $request->getParsedBody()['name'] ?? 'anonymous'; - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array(), + return React\Http\Message\Response::plaintext( "Hello $name!\n" ); }); @@ -1102,10 +1080,8 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $data = json_decode((string)$request->getBody()); $name = $data->name ?? 'anonymous'; - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array('Content-Type' => 'application/json'), - json_encode(['message' => "Hello $name!"]) + return React\Http\Message\Response::json( + ['message' => "Hello $name!"] ); }); ``` @@ -1125,9 +1101,7 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $files = $request->getUploadedFiles(); $name = isset($files['avatar']) ? $files['avatar']->getClientFilename() : 'nothing'; - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array(), + return React\Http\Message\Response::plaintext( "Uploaded $name\n" ); }); @@ -1208,24 +1182,16 @@ $http = new React\Http\HttpServer( }); $body->on('end', function () use ($resolve, &$bytes){ - $resolve(new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + $resolve(React\Http\Message\Response::plaintext( "Received $bytes bytes\n" )); }); // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event $body->on('error', function (Exception $e) use ($resolve, &$bytes) { - $resolve(new React\Http\Message\Response( - React\Http\Message\Response::STATUS_BAD_REQUEST, - array( - 'Content-Type' => 'text/plain' - ), + $resolve(React\Http\Message\Response::plaintext( "Encountered error after $bytes bytes: {$e->getMessage()}\n" - )); + )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST)); }); }); } @@ -1272,23 +1238,15 @@ $http = new React\Http\HttpServer( function (Psr\Http\Message\ServerRequestInterface $request) { $size = $request->getBody()->getSize(); if ($size === null) { - $body = 'The request does not contain an explicit length.'; - $body .= 'This example does not accept chunked transfer encoding.'; - - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_LENGTH_REQUIRED, - array( - 'Content-Type' => 'text/plain' - ), + $body = "The request does not contain an explicit length. "; + $body .= "This example does not accept chunked transfer encoding.\n"; + + return React\Http\Message\Response::plaintext( $body - ); + )->withStatus(React\Http\Message\Response::STATUS_LENGTH_REQUIRED); } - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( "Request body size: " . $size . " bytes\n" ); } @@ -1344,25 +1302,16 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf $key = 'react\php'; if (isset($request->getCookieParams()[$key])) { - $body = "Your cookie value is: " . $request->getCookieParams()[$key]; + $body = "Your cookie value is: " . $request->getCookieParams()[$key] . "\n"; - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( $body ); } - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain', - 'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more') - ), - "Your cookie has been set." - ); + return React\Http\Message\Response::plaintext( + "Your cookie has been set.\n" + )->withHeader('Set-Cookie', urlencode($key) . '=' . urlencode('test;more')); }); ``` @@ -1413,11 +1362,7 @@ In its most simple form, you can use it like this: ```php $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( "Hello World!\n" ); }); @@ -1441,18 +1386,17 @@ This example shows how such a long-term action could look like: ```php $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { - return new Promise(function ($resolve, $reject) { + $promise = new Promise(function ($resolve, $reject) { Loop::addTimer(1.5, function() use ($resolve) { - $response = new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), - "Hello world" - ); - $resolve($response); + $resolve(); }); }); + + return $promise->then(function () { + return React\Http\Message\Response::plaintext( + "Hello World!" + ); + }); }); ``` @@ -1571,11 +1515,7 @@ a `string` response body like this: ```php $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( "Hello World!\n" ); }); @@ -1845,11 +1785,9 @@ $http = new React\Http\HttpServer( $resolve($next($request)); }); return $promise->then(null, function (Exception $e) { - return new React\Http\Message\Response( - React\Http\Message\Response::STATUS_INTERNAL_SERVER_ERROR, - array(), - 'Internal error: ' . $e->getMessage() - ); + return React\Http\Message\Response::plaintext( + 'Internal error: ' . $e->getMessage() . "\n" + )->withStatus(React\Http\Message\Response::STATUS_INTERNAL_SERVER_ERROR); }); }, function (Psr\Http\Message\ServerRequestInterface $request) { diff --git a/examples/51-server-hello-world.php b/examples/51-server-hello-world.php index 88831525..9ff84eee 100644 --- a/examples/51-server-hello-world.php +++ b/examples/51-server-hello-world.php @@ -1,17 +1,10 @@ 'text/plain' - ), - "Hello world\n" +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + return React\Http\Message\Response::plaintext( + "Hello World!\n" ); }); diff --git a/examples/52-server-count-visitors.php b/examples/52-server-count-visitors.php index bdd53af9..341f9498 100644 --- a/examples/52-server-count-visitors.php +++ b/examples/52-server-count-visitors.php @@ -1,17 +1,10 @@ 'text/plain' - ), +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) use (&$counter) { + return React\Http\Message\Response::plaintext( "Welcome number " . ++$counter . "!\n" ); }); diff --git a/examples/53-server-whatsmyip.php b/examples/53-server-whatsmyip.php index e0835a82..1e394b9e 100644 --- a/examples/53-server-whatsmyip.php +++ b/examples/53-server-whatsmyip.php @@ -1,18 +1,11 @@ getServerParams()['REMOTE_ADDR']; +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR'] . "\n"; - return new Response( - Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( $body ); }); diff --git a/examples/54-server-query-parameter.php b/examples/54-server-query-parameter.php index 18dd56b0..9b2d5749 100644 --- a/examples/54-server-query-parameter.php +++ b/examples/54-server-query-parameter.php @@ -1,11 +1,8 @@ getQueryParams(); $body = 'The query parameter "foo" is not set. Click the following link '; @@ -15,11 +12,7 @@ $body = 'The value of "foo" is: ' . htmlspecialchars($queryParams['foo']); } - return new Response( - Response::STATUS_OK, - array( - 'Content-Type' => 'text/html' - ), + return React\Http\Message\Response::html( $body ); }); diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index 8260fc33..796da24d 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -1,33 +1,21 @@ getCookieParams()[$key])) { - $body = "Your cookie value is: " . $request->getCookieParams()[$key]; + $body = "Your cookie value is: " . $request->getCookieParams()[$key] . "\n"; - return new Response( - Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + return React\Http\Message\Response::plaintext( $body ); } - return new Response( - Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain', - 'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more') - ), - "Your cookie has been set." - ); + return React\Http\Message\Response::plaintext( + "Your cookie has been set.\n" + )->withHeader('Set-Cookie', urlencode($key) . '=' . urlencode('test;more')); }); $socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); diff --git a/examples/56-server-sleep.php b/examples/56-server-sleep.php index 6bb6f82b..2a3c9027 100644 --- a/examples/56-server-sleep.php +++ b/examples/56-server-sleep.php @@ -1,25 +1,21 @@ 'text/plain' - ), - "Hello world" - ); - $resolve($response); + $resolve(); }); }); + + return $promise->then(function () { + return React\Http\Message\Response::plaintext( + "Hello world!\n" + ); + }); }); $socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); diff --git a/examples/57-server-error-handling.php b/examples/57-server-error-handling.php index 71cbad15..a9fb6bad 100644 --- a/examples/57-server-error-handling.php +++ b/examples/57-server-error-handling.php @@ -1,30 +1,18 @@ 'text/plain' - ), - "Hello World!\n" - ); + if ($count % 2 === 0) { + throw new Exception('Second call'); + } - $resolve($response); - }); + return React\Http\Message\Response::plaintext( + "Hello World!\n" + ); }); $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 015ddd9a..cf65a3bf 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -1,13 +1,12 @@ getMethod() !== 'GET' || $request->getUri()->getPath() !== '/') { return new Response(Response::STATUS_NOT_FOUND); } diff --git a/examples/59-server-json-api.php b/examples/59-server-json-api.php index 0d50b52b..f48be7e3 100644 --- a/examples/59-server-json-api.php +++ b/examples/59-server-json-api.php @@ -6,49 +6,32 @@ // $ php examples/59-server-json-api.php 8080 // $ curl -v http://localhost:8080/ -H 'Content-Type: application/json' -d '{"name":"Alice"}' -use Psr\Http\Message\ServerRequestInterface; use React\Http\Message\Response; require __DIR__ . '/../vendor/autoload.php'; -$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { if ($request->getHeaderLine('Content-Type') !== 'application/json') { - return new Response( - Response::STATUS_UNSUPPORTED_MEDIA_TYPE, - array( - 'Content-Type' => 'application/json' - ), - json_encode(array('error' => 'Only supports application/json')) . "\n" - ); + return Response::json( + array('error' => 'Only supports application/json') + )->withStatus(Response::STATUS_UNSUPPORTED_MEDIA_TYPE); } $input = json_decode($request->getBody()->getContents()); if (json_last_error() !== JSON_ERROR_NONE) { - return new Response( - Response::STATUS_BAD_REQUEST, - array( - 'Content-Type' => 'application/json' - ), - json_encode(array('error' => 'Invalid JSON data given')) . "\n" - ); + return Response::json( + array('error' => 'Invalid JSON data given') + )->withStatus(Response::STATUS_BAD_REQUEST); } if (!isset($input->name) || !is_string($input->name)) { - return new Response( - Response::STATUS_UNPROCESSABLE_ENTITY, - array( - 'Content-Type' => 'application/json' - ), - json_encode(array('error' => 'JSON data does not contain a string "name" property')) . "\n" - ); + return Response::json( + array('error' => 'JSON data does not contain a string "name" property') + )->withStatus(Response::STATUS_UNPROCESSABLE_ENTITY); } - return new Response( - Response::STATUS_OK, - array( - 'Content-Type' => 'application/json' - ), - json_encode(array('message' => 'Hello ' . $input->name)) . "\n" + return Response::json( + array('message' => 'Hello ' . $input->name) ); }); diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index 2fd6f9af..23906430 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -1,17 +1,10 @@ 'text/plain' - ), - "Hello world!\n" +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + return React\Http\Message\Response::plaintext( + "Hello World!\n" ); }); diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index 899caa0a..52864c82 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -109,11 +109,7 @@ HTML; - return new Response( - Response::STATUS_OK, - array( - 'Content-Type' => 'text/html; charset=UTF-8' - ), + return Response::html( $html ); }; diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index b20b8f08..fef6f008 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -19,24 +19,16 @@ function (Psr\Http\Message\ServerRequestInterface $request) { }); $body->on('end', function () use ($resolve, &$bytes){ - $resolve(new React\Http\Message\Response( - React\Http\Message\Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), + $resolve(React\Http\Message\Response::plaintext( "Received $bytes bytes\n" )); }); // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event $body->on('error', function (Exception $e) use ($resolve, &$bytes) { - $resolve(new React\Http\Message\Response( - React\Http\Message\Response::STATUS_BAD_REQUEST, - array( - 'Content-Type' => 'text/plain' - ), + $resolve(React\Http\Message\Response::plaintext( "Encountered error after $bytes bytes: {$e->getMessage()}\n" - )); + )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST)); }); }); } diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index e0bf8404..cf63c4ae 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -3,25 +3,17 @@ // $ php examples/71-server-http-proxy.php 8080 // $ curl -v --proxy http://localhost:8080 http://reactphp.org/ -use Psr\Http\Message\RequestInterface; -use React\Http\Message\Response; -use RingCentral\Psr7; - require __DIR__ . '/../vendor/autoload.php'; // Note how this example uses the `HttpServer` without the `StreamingRequestMiddleware`. // This means that this proxy buffers the whole request before "processing" it. // As such, this is store-and-forward proxy. This could also use the advanced // `StreamingRequestMiddleware` to forward the incoming request as it comes in. -$http = new React\Http\HttpServer(function (RequestInterface $request) { +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { if (strpos($request->getRequestTarget(), '://') === false) { - return new Response( - Response::STATUS_BAD_REQUEST, - array( - 'Content-Type' => 'text/plain' - ), - 'This is a plain HTTP proxy' - ); + return React\Http\Message\Response::plaintext( + "This is a plain HTTP proxy\n" + )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); } // prepare outgoing client request by updating request-target and Host header @@ -35,12 +27,8 @@ // pseudo code only: simply dump the outgoing request as a string // 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 new Response( - Response::STATUS_OK, - array( - 'Content-Type' => 'text/plain' - ), - Psr7\str($outgoing) + return React\Http\Message\Response::plaintext( + RingCentral\Psr7\str($outgoing) ); }); diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index 6c737605..ddd4760a 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -93,11 +93,7 @@ public function getSize() $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { switch ($request->getUri()->getPath()) { case '/': - return new Response( - Response::STATUS_OK, - array( - 'Content-Type' => 'text/html' - ), + return Response::html( '1g.bin
10g.bin' ); case '/1g.bin': From 59961cc4a5b14481728f07c591546be18fa3a5c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 3 Feb 2022 14:17:37 +0100 Subject: [PATCH 086/152] Prepare v1.6.0 release --- CHANGELOG.md | 42 ++++++++++++++++++++++++++++++++++++++++++ README.md | 12 ++++++------ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e12b787e..41079cdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,47 @@ # Changelog +## 1.6.0 (2022-02-03) + +* Feature: Add factory methods for common HTML/JSON/plaintext/XML response types. + (#439 by @clue) + + ```php + $response = React\Http\Response\html("

Hello wörld!

\n"); + $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. + (#432 by @clue) + + ```php + $response = new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, // 200 OK + … + ); + $response = new React\Http\Message\Response( + React\Http\Message\Response::STATUS_NOT_FOUND, // 404 Not Found + … + ); + ``` + +* Feature: Full support for PHP 8.1 release. + (#433 by @SimonFrings and #434 by @clue) + +* Feature / Fix: Improve protocol handling for HTTP responses with no body. + (#429 and #430 by @clue) + +* Internal refactoring and internal improvements for handling requests and responses. + (#422 by @WyriHaximus and #431 by @clue) + +* Improve documentation, update proxy examples, include error reporting in examples. + (#420, #424, #426, and #427 by @clue) + +* Update test suite to use default loop. + (#438 by @clue) + ## 1.5.0 (2021-08-04) * Feature: Update `Browser` signature to take optional `$connector` as first argument and diff --git a/README.md b/README.md index 23e4b5fa..8e58aebc 100644 --- a/README.md +++ b/README.md @@ -2904,14 +2904,14 @@ new React\Http\Middleware\RequestBodyParserMiddleware(10 * 1024, 100); // 100 fi ## Install -The recommended way to install this library is [through Composer](https://getcomposer.org). +The recommended way to install this library is [through Composer](https://getcomposer.org/). [New to Composer?](https://getcomposer.org/doc/00-intro.md) This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require react/http:^1.5 +$ composer require react/http:^1.6 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. @@ -2919,12 +2919,12 @@ 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.3 through current PHP 8+ and HHVM. -It's *highly recommended to use PHP 7+* for this project. +It's *highly recommended to use the latest supported PHP version* for this project. ## Tests To run the test suite, you first need to clone this repo and then install all -dependencies [through Composer](https://getcomposer.org): +dependencies [through Composer](https://getcomposer.org/): ```bash $ composer install @@ -2933,7 +2933,7 @@ $ composer install To run the test suite, go to the project root and run: ```bash -$ php vendor/bin/phpunit +$ vendor/bin/phpunit ``` The test suite also contains a number of functional integration tests that rely @@ -2941,7 +2941,7 @@ 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 +$ vendor/bin/phpunit --exclude-group internet ``` ## License From 83162766662e19f3367d8d30da41e1c07c39c5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 4 Feb 2022 08:56:40 +0100 Subject: [PATCH 087/152] Skip memory tests when lowering memory limit does not work --- tests/HttpServerTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/HttpServerTest.php b/tests/HttpServerTest.php index 23dcc758..a6d8057b 100644 --- a/tests/HttpServerTest.php +++ b/tests/HttpServerTest.php @@ -404,7 +404,9 @@ public function testConstructServerWithUnlimitedMemoryLimitDoesNotLimitConcurren public function testConstructServerWithMemoryLimitDoesLimitConcurrency() { $old = ini_get('memory_limit'); - ini_set('memory_limit', '100M'); + if (@ini_set('memory_limit', '128M') === false) { + $this->markTestSkipped('Unable to change memory limit'); + } $http = new HttpServer(function () { }); From 185680b8ca0704051e5c9ab9e7b98b53820b5aef Mon Sep 17 00:00:00 2001 From: Simon Bennett Date: Fri, 4 Mar 2022 13:47:33 +0000 Subject: [PATCH 088/152] [Bug] Allow explicit assigning of `Content-Length` for `HEAD` requests --- src/Io/StreamingServer.php | 2 ++ tests/Io/StreamingServerTest.php | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 7818f0bd..d73d527d 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -265,6 +265,8 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt if (($method === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === Response::STATUS_NO_CONTENT) { // 2xx response to CONNECT and 1xx and 204 MUST NOT include Content-Length or Transfer-Encoding header $response = $response->withoutHeader('Content-Length'); + } elseif ($method === 'HEAD' && $response->hasHeader('Content-Length')) { + // HEAD Request: preserve explicit Content-Length } elseif ($code === Response::STATUS_NOT_MODIFIED && ($response->hasHeader('Content-Length') || $body->getSize() === 0)) { // 304 Not Modified: preserve explicit Content-Length and preserve missing header if body is empty } elseif ($body->getSize() !== null) { diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 59cfd118..45729f2b 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -1426,6 +1426,38 @@ function ($data) use (&$buffer) { $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); } + public function testResponseContainsExplicitContentLengthHeaderForHeadRequests() + { + $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { + return new Response( + 200, + array('Content-Length' => 3), + '' + ); + }); + + $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 = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; + $this->connection->emit('data', array($data)); + + $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); + } + public function testResponseContainsNoResponseBodyForNotModifiedStatus() { $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { From 201021fe3fafa2a6dda9ded7b2759653224418aa Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Tue, 15 Mar 2022 13:31:28 +0100 Subject: [PATCH 089/152] Add badge to show number of project installations --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8e58aebc..154f4901 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # HTTP [![CI status](https://github.com/reactphp/http/workflows/CI/badge.svg)](https://github.com/reactphp/http/actions) +[![installs on Packagist](https://img.shields.io/packagist/dt/react/http?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/http) Event-driven, streaming HTTP client and server implementation for [ReactPHP](https://reactphp.org/). From 8fbdc0cec0cd754a6cdeb3e29e501d8dbd28b591 Mon Sep 17 00:00:00 2001 From: Jorrit Schippers Date: Tue, 5 Apr 2022 23:01:57 +0200 Subject: [PATCH 090/152] README.md: fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 154f4901..56acb365 100644 --- a/README.md +++ b/README.md @@ -389,7 +389,7 @@ try { $response = Block\await($promise, Loop::get()); // response successfully received } catch (Exception $e) { - // an error occured while performing the request + // an error occurred while performing the request } ``` From dd2cb536e7dc071bebe6d78e93f4f58de6f931a4 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Tue, 12 Apr 2022 09:41:48 +0200 Subject: [PATCH 091/152] Fix legacy HHVM build by downgrading Composer --- .github/workflows/ci.yml | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c64ef6ab..27577838 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,5 +43,6 @@ jobs: - uses: azjezz/setup-hhvm@v1 with: version: lts-3.30 + - run: composer self-update --2.2 # downgrade Composer for HHVM - run: hhvm $(which composer) install - run: hhvm vendor/bin/phpunit diff --git a/README.md b/README.md index 154f4901..33ec6967 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # HTTP -[![CI status](https://github.com/reactphp/http/workflows/CI/badge.svg)](https://github.com/reactphp/http/actions) +[![CI status](https://github.com/reactphp/http/actions/workflows/ci.yml/badge.svg)](https://github.com/reactphp/http/actions) [![installs on Packagist](https://img.shields.io/packagist/dt/react/http?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/http) Event-driven, streaming HTTP client and server implementation for [ReactPHP](https://reactphp.org/). From a6bc13dfd46eee82fc45796f0350135a06123f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 14 Apr 2022 13:28:22 +0200 Subject: [PATCH 092/152] Improve documentation for closing response stream --- README.md | 10 +++++++++- examples/58-server-stream-response.php | 8 +++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 56acb365..aa26deb3 100644 --- a/README.md +++ b/README.md @@ -1426,15 +1426,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( 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( From 9c2d98f1f5b590082faa1a74aba5549cd0107977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 19 May 2022 19:30:38 +0200 Subject: [PATCH 093/152] Improve performance, add internal `Clock`, reuse clock in same tick --- src/Io/Clock.php | 54 ++++++++++ src/Io/RequestHeaderParser.php | 12 ++- src/Io/StreamingServer.php | 11 +- tests/HttpServerTest.php | 8 +- tests/Io/ClockTest.php | 43 ++++++++ tests/Io/RequestHeaderParserTest.php | 156 +++++++++++++++++++-------- tests/Io/StreamingServerTest.php | 33 +++--- 7 files changed, 254 insertions(+), 63 deletions(-) create mode 100644 src/Io/Clock.php create mode 100644 tests/Io/ClockTest.php 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/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index e5554c46..6930afaf 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -24,6 +24,14 @@ class RequestHeaderParser extends EventEmitter { private $maxSize = 8192; + /** @var Clock */ + private $clock; + + public function __construct(Clock $clock) + { + $this->clock = $clock; + } + public function handle(ConnectionInterface $conn) { $buffer = ''; @@ -155,8 +163,8 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) // 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) + 'REQUEST_TIME' => (int) ($now = $this->clock->now()), + 'REQUEST_TIME_FLOAT' => $now ); // scheme is `http` unless TLS is used diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index d73d527d..a054be3d 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -84,7 +84,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 +106,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) { @@ -255,7 +256,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'); } diff --git a/tests/HttpServerTest.php b/tests/HttpServerTest.php index a6d8057b..4d00fcef 100644 --- a/tests/HttpServerTest.php +++ b/tests/HttpServerTest.php @@ -54,9 +54,13 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() $ref->setAccessible(true); $streamingServer = $ref->getValue($http); - $ref = new \ReflectionProperty($streamingServer, 'loop'); + $ref = new \ReflectionProperty($streamingServer, 'clock'); $ref->setAccessible(true); - $loop = $ref->getValue($streamingServer); + $clock = $ref->getValue($streamingServer); + + $ref = new \ReflectionProperty($clock, 'loop'); + $ref->setAccessible(true); + $loop = $ref->getValue($clock); $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); } 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/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index 356443fb..7ba7fe01 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -10,7 +10,9 @@ 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); @@ -742,7 +812,9 @@ 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/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 45729f2b..2703362a 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -2292,12 +2292,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 +2326,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( @@ -3022,7 +3030,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 +3054,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 +3071,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 +3094,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 +3119,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 +3144,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 +3169,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 +3195,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 +3221,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'); From dc8ca43cc58dd3555d107d89bd5a9e36c13d4e7d Mon Sep 17 00:00:00 2001 From: Nicolas Hedger <649677+nhedger@users.noreply.github.com> Date: Tue, 14 Jun 2022 20:29:41 +0200 Subject: [PATCH 094/152] chore(docs): remove $ sign from shell commands --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d3a93ceb..2d2eb4ce 100644 --- a/README.md +++ b/README.md @@ -2920,7 +2920,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.6 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. @@ -2936,13 +2936,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 @@ -2950,7 +2950,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 From 8b2c5c899ba47ac357cd451752a1079a4386d158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 18 Jun 2022 12:10:39 +0200 Subject: [PATCH 095/152] Refactor internal `Transaction` to avoid assigning dynamic properties --- src/Io/ClientRequestState.php | 16 +++++++ src/Io/Transaction.php | 79 +++++++++++++++----------------- tests/HttpServerTest.php | 3 ++ tests/Io/StreamingServerTest.php | 3 ++ 4 files changed, 59 insertions(+), 42 deletions(-) create mode 100644 src/Io/ClientRequestState.php 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 @@ +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 +105,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,53 +119,51 @@ 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(); @@ -205,26 +202,24 @@ function ($e) use ($stream, $maximumSize) { } ); - $deferred->pending = $promise; + $state->pending = $promise; return $promise; } /** * @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 @@ -242,7 +237,7 @@ public function onResponse(ResponseInterface $response, RequestInterface $reques * @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')); @@ -250,11 +245,11 @@ private function onResponseRedirect(ResponseInterface $response, RequestInterfac $request = $this->makeRedirectRequest($request, $location); $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); } /** diff --git a/tests/HttpServerTest.php b/tests/HttpServerTest.php index 4d00fcef..31bc32ee 100644 --- a/tests/HttpServerTest.php +++ b/tests/HttpServerTest.php @@ -17,6 +17,9 @@ final class HttpServerTest extends TestCase private $connection; private $socket; + /** @var ?int */ + private $called = null; + /** * @before */ diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 2703362a..a2700b86 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -17,6 +17,9 @@ class StreamingServerTest extends TestCase private $connection; private $socket; + /** @var ?int */ + private $called = null; + /** * @before */ From 90413fb088d701f77f12e50e5f471e3f000aca2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 9 Aug 2022 10:50:19 +0200 Subject: [PATCH 096/152] Avoid referencing unneeded explicit loop instance --- README.md | 4 ++-- tests/Io/SenderTest.php | 32 ++++++++++++++++++++++++-------- tests/Io/TransactionTest.php | 26 ++++++++++++++++---------- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 2d2eb4ce..752c01ff 100644 --- a/README.md +++ b/README.md @@ -386,7 +386,7 @@ $browser = new React\Http\Browser(); $promise = $browser->get('http://example.com/'); try { - $response = Block\await($promise, Loop::get()); + $response = Block\await($promise); // response successfully received } catch (Exception $e) { // an error occurred while performing the request @@ -401,7 +401,7 @@ $promises = array( $browser->get('http://www.example.org/'), ); -$responses = Block\awaitAll($promises, Loop::get()); +$responses = Block\awaitAll($promises); ``` Please refer to [clue/reactphp-block](https://github.com/clue/reactphp-block#readme) for more details. diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 1c6d1d6b..8597f17a 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -42,8 +42,12 @@ public function testSenderRejectsInvalidUri() $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() @@ -57,8 +61,12 @@ public function testSenderConnectorRejection() $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() @@ -318,8 +326,12 @@ public function testCancelRequestWillCancelConnector() $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() @@ -337,8 +349,12 @@ public function testCancelRequestWillCloseConnection() $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 provideRequestProtocolVersion() diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index d62147b5..835e299c 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -5,7 +5,8 @@ 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\ResponseException; use React\EventLoop\Loop; @@ -14,7 +15,7 @@ use React\Stream\ThroughStream; use React\Tests\Http\TestCase; use RingCentral\Psr7\Request; -use React\Http\Io\ReadableBodyStream; +use RingCentral\Psr7\Response; class TransactionTest extends TestCase { @@ -372,13 +373,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() @@ -461,8 +463,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()); } From a2ae0f1eb655ec9766888c121f4182faba44dac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 8 Aug 2022 19:20:55 +0200 Subject: [PATCH 097/152] Avoid using deprecated functions from clue/reactphp-block --- composer.json | 3 +- tests/Client/FunctionalIntegrationTest.php | 10 ++--- tests/FunctionalBrowserTest.php | 2 +- tests/FunctionalHttpServerTest.php | 46 +++++++++++----------- tests/Io/TransactionTest.php | 4 +- 5 files changed, 33 insertions(+), 32 deletions(-) diff --git a/composer.json b/composer.json index 4c9a0383..0d23e281 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,8 @@ "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" + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "react/promise-timer": "^1.9" }, "autoload": { "psr-4": { "React\\Http\\": "src" } diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 64a3ea8a..40b40a54 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -51,7 +51,7 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() $promise = Stream\first($request, 'close'); $request->end(); - Block\await($promise, null, self::TIMEOUT_LOCAL); + Block\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); } public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResponse() @@ -73,7 +73,7 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp $promise = Stream\first($request, 'close'); $request->end(); - Block\await($promise, null, self::TIMEOUT_LOCAL); + Block\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); } /** @group internet */ @@ -94,7 +94,7 @@ public function testSuccessfulResponseEmitsEnd() $promise = Stream\first($request, 'close'); $request->end(); - Block\await($promise, null, self::TIMEOUT_REMOTE); + Block\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_REMOTE)); } /** @group internet */ @@ -122,7 +122,7 @@ public function testPostDataReturnsData() $request->end($data); - $buffer = Block\await($deferred->promise(), null, self::TIMEOUT_REMOTE); + $buffer = Block\await(\React\Promise\Timer\timeout($deferred->promise(), self::TIMEOUT_REMOTE)); $this->assertNotEquals('', $buffer); @@ -154,7 +154,7 @@ public function testPostJsonReturnsData() $request->end($data); - $buffer = Block\await($deferred->promise(), null, self::TIMEOUT_REMOTE); + $buffer = Block\await(\React\Promise\Timer\timeout($deferred->promise(), self::TIMEOUT_REMOTE)); $this->assertNotEquals('', $buffer); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 2b7bd58c..a9cc7244 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -531,7 +531,7 @@ public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeeps $response = Block\await($this->browser->get($this->base . 'get', array())); $this->assertEquals('hello', (string)$response->getBody()); - $ret = Block\await($closed->promise(), null, 0.1); + $ret = Block\await(\React\Promise\Timer\timeout($closed->promise(), 0.1)); $this->assertTrue($ret); $socket->close(); diff --git a/tests/FunctionalHttpServerTest.php b/tests/FunctionalHttpServerTest.php index 6fa85903..f543fb55 100644 --- a/tests/FunctionalHttpServerTest.php +++ b/tests/FunctionalHttpServerTest.php @@ -37,7 +37,7 @@ public function testPlainHttpOnRandomPort() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\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 +64,7 @@ function () { return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 404 Not Found", $response); @@ -88,7 +88,7 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\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 +113,7 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\await(\React\Promise\Timer\timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://localhost:1000/', $response); @@ -146,7 +146,7 @@ public function testSecureHttpsOnRandomPort() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\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 +183,7 @@ public function testSecureHttpsReturnsData() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\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 +217,7 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\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 +246,7 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\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 +275,7 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\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 +313,7 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\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 +351,7 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\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 +380,7 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\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 +418,7 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\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 +446,7 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\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 +477,7 @@ function (RequestInterface $request) use ($once) { }); }); - Block\sleep(0.1); + \Clue\React\Block\await(\React\Promise\Timer\sleep(0.1)); $socket->close(); } @@ -507,7 +507,7 @@ function (RequestInterface $request) use ($stream) { }); // stream will be closed within 0.1s - $ret = Block\await(Stream\first($stream, 'close'), null, 0.1); + $ret = Block\await(\React\Promise\Timer\timeout(Stream\first($stream, 'close'), 0.1)); $socket->close(); @@ -536,7 +536,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() }); // await response stream to be closed - $ret = Block\await(Stream\first($stream, 'close'), null, 1.0); + $ret = Block\await(\React\Promise\Timer\timeout(Stream\first($stream, 'close'), 1.0)); $socket->close(); @@ -571,7 +571,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\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 +608,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\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 +644,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\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 +684,7 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\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 +717,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() return Stream\buffer($conn); }); - $response = Block\await($result, null, 1.0); + $response = Block\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); @@ -760,7 +760,7 @@ function (ServerRequestInterface $request) { }); } - $responses = Block\await(Promise\all($result), null, 1.0); + $responses = Block\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/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 835e299c..56b9d4f4 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -424,7 +424,7 @@ public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBuffer $promise = $transaction->send($request); $this->setExpectedException('OverflowException'); - Block\await($promise, null, 0.001); + Block\await(\React\Promise\Timer\timeout($promise, 0.001)); } public function testCancelBufferingResponseWillCloseStreamAndReject() @@ -445,7 +445,7 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() $promise->cancel(); $this->setExpectedException('RuntimeException'); - Block\await($promise, null, 0.001); + Block\await(\React\Promise\Timer\timeout($promise, 0.001)); } public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStreamingIsEnabled() From 9946ba7bb7330cf72f96d744608fd0acea6fc7d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 9 Aug 2022 10:55:31 +0200 Subject: [PATCH 098/152] Update to use new reactphp/async package instead of clue/reactphp-block --- README.md | 20 +-- composer.json | 2 +- tests/BrowserTest.php | 1 - tests/Client/FunctionalIntegrationTest.php | 11 +- tests/FunctionalBrowserTest.php | 123 ++++++++++-------- tests/FunctionalHttpServerTest.php | 47 ++++--- tests/HttpServerTest.php | 7 +- tests/Io/MiddlewareRunnerTest.php | 5 +- tests/Io/SenderTest.php | 1 - tests/Io/TransactionTest.php | 7 +- .../RequestBodyBufferMiddlewareTest.php | 9 +- 11 files changed, 123 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index 752c01ff..ba8d8330 100644 --- a/README.md +++ b/README.md @@ -373,20 +373,19 @@ 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); + $response = await($promise); // response successfully received } catch (Exception $e) { // an error occurred while performing the request @@ -396,15 +395,20 @@ try { 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); +$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 diff --git a/composer.json b/composer.json index 0d23e281..57adfc2c 100644 --- a/composer.json +++ b/composer.json @@ -38,11 +38,11 @@ "ringcentral/psr7": "^1.2" }, "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", + "react/async": "^4 || ^3 || ^2", "react/promise-timer": "^1.9" }, "autoload": { diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index 39be453a..f14b9ee6 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -2,7 +2,6 @@ namespace React\Tests\Http; -use Clue\React\Block; use Psr\Http\Message\RequestInterface; use React\Http\Browser; use React\Promise\Promise; diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 40b40a54..d95bf828 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -2,7 +2,6 @@ namespace React\Tests\Http\Client; -use Clue\React\Block; use Psr\Http\Message\ResponseInterface; use React\EventLoop\Loop; use React\Http\Client\Client; @@ -51,7 +50,7 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() $promise = Stream\first($request, 'close'); $request->end(); - Block\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); + \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); } public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResponse() @@ -73,7 +72,7 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp $promise = Stream\first($request, 'close'); $request->end(); - Block\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); + \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); } /** @group internet */ @@ -94,7 +93,7 @@ public function testSuccessfulResponseEmitsEnd() $promise = Stream\first($request, 'close'); $request->end(); - Block\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_REMOTE)); + \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_REMOTE)); } /** @group internet */ @@ -122,7 +121,7 @@ public function testPostDataReturnsData() $request->end($data); - $buffer = Block\await(\React\Promise\Timer\timeout($deferred->promise(), self::TIMEOUT_REMOTE)); + $buffer = \React\Async\await(\React\Promise\Timer\timeout($deferred->promise(), self::TIMEOUT_REMOTE)); $this->assertNotEquals('', $buffer); @@ -154,7 +153,7 @@ public function testPostJsonReturnsData() $request->end($data); - $buffer = Block\await(\React\Promise\Timer\timeout($deferred->promise(), self::TIMEOUT_REMOTE)); + $buffer = \React\Async\await(\React\Promise\Timer\timeout($deferred->promise(), self::TIMEOUT_REMOTE)); $this->assertNotEquals('', $buffer); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index a9cc7244..95092ac1 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -2,7 +2,6 @@ namespace React\Tests\Http; -use Clue\React\Block; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; @@ -24,6 +23,9 @@ class FunctionalBrowserTest extends TestCase private $browser; private $base; + /** @var ?SocketServer */ + private $socket; + /** * @before */ @@ -88,14 +90,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 +145,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 +166,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 +174,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 +182,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 +190,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 +199,7 @@ public function testCancelGetRequestWillRejectRequest() $promise->cancel(); $this->setExpectedException('RuntimeException'); - Block\await($promise); + \React\Async\await($promise); } public function testCancelRequestWithPromiseFollowerWillRejectRequest() @@ -195,13 +210,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,7 +226,7 @@ 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')); } /** @@ -225,7 +240,7 @@ 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))); } /** @@ -240,7 +255,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 +267,7 @@ public function testCancelRedirectedRequestShouldReject() }); $this->setExpectedException('RuntimeException', 'Request cancelled'); - Block\await($promise); + \React\Async\await($promise); } public function testTimeoutDelayedResponseShouldReject() @@ -260,7 +275,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 +285,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 +293,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 +301,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 +309,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 +319,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 +327,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 +342,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 +357,7 @@ public function testGetRequestWithResponseBufferMatchedExactlyResolves() { $promise = $this->browser->withResponseBuffer(5)->get($this->base . 'get'); - Block\await($promise); + \React\Async\await($promise); } public function testGetRequestWithResponseBufferExceededRejects() @@ -354,7 +369,7 @@ public function testGetRequestWithResponseBufferExceededRejects() 'Response body size of 5 bytes exceeds maximum of 4 bytes', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 ); - Block\await($promise); + \React\Async\await($promise); } public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() @@ -366,7 +381,7 @@ public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() 'Response body size exceeds maximum of 4 bytes', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 ); - Block\await($promise); + \React\Async\await($promise); } /** @@ -379,7 +394,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 +415,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 +436,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 +445,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 +463,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 +478,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 +489,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 +501,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 +520,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,10 +545,10 @@ 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(\React\Promise\Timer\timeout($closed->promise(), 0.1)); + $ret = \React\Async\await(\React\Promise\Timer\timeout($closed->promise(), 0.1)); $this->assertTrue($ret); $socket->close(); @@ -545,7 +562,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 +578,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 +598,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 +608,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 +628,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 +648,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 +656,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 +666,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 +677,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 +688,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 +699,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 f543fb55..eb3b448c 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(\React\Promise\Timer\timeout($result, 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(\React\Promise\Timer\timeout($result, 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(\React\Promise\Timer\timeout($result, 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(\React\Promise\Timer\timeout($result, 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(\React\Promise\Timer\timeout($result, 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(\React\Promise\Timer\timeout($result, 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(\React\Promise\Timer\timeout($result, 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(\React\Promise\Timer\timeout($result, 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(\React\Promise\Timer\timeout($result, 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(\React\Promise\Timer\timeout($result, 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(\React\Promise\Timer\timeout($result, 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(\React\Promise\Timer\timeout($result, 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(\React\Promise\Timer\timeout($result, 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(\React\Promise\Timer\timeout($result, 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) { }); }); - \Clue\React\Block\await(\React\Promise\Timer\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(\React\Promise\Timer\timeout(Stream\first($stream, 'close'), 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(\React\Promise\Timer\timeout(Stream\first($stream, 'close'), 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(\React\Promise\Timer\timeout($result, 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(\React\Promise\Timer\timeout($result, 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(\React\Promise\Timer\timeout($result, 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(\React\Promise\Timer\timeout($result, 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(\React\Promise\Timer\timeout($result, 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); @@ -760,7 +759,7 @@ function (ServerRequestInterface $request) { }); } - $responses = Block\await(\React\Promise\Timer\timeout(Promise\all($result), 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 31bc32ee..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; @@ -142,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(); @@ -180,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()); @@ -213,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/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index d8f5f232..1f49facd 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -2,7 +2,6 @@ namespace React\Tests\Http\Io; -use Clue\React\Block; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -161,7 +160,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 +227,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); diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 8597f17a..587ba0c2 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -2,7 +2,6 @@ namespace React\Tests\Http\Io; -use Clue\React\Block; use React\Http\Client\Client as HttpClient; use React\Http\Client\RequestData; use React\Http\Io\ReadableBodyStream; diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 56b9d4f4..83d218c7 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -2,7 +2,6 @@ namespace React\Tests\Http\Io; -use Clue\React\Block; use PHPUnit\Framework\MockObject\MockObject; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; @@ -401,7 +400,7 @@ 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()); @@ -424,7 +423,7 @@ public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBuffer $promise = $transaction->send($request); $this->setExpectedException('OverflowException'); - Block\await(\React\Promise\Timer\timeout($promise, 0.001)); + \React\Async\await(\React\Promise\Timer\timeout($promise, 0.001)); } public function testCancelBufferingResponseWillCloseStreamAndReject() @@ -445,7 +444,7 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() $promise->cancel(); $this->setExpectedException('RuntimeException'); - Block\await(\React\Promise\Timer\timeout($promise, 0.001)); + \React\Async\await(\React\Promise\Timer\timeout($promise, 0.001)); } public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStreamingIsEnabled() diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index e073e1f0..0edec7da 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -2,7 +2,6 @@ namespace React\Tests\Http\Middleware; -use Clue\React\Block; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\Http\Io\HttpBodyStream; @@ -128,7 +127,7 @@ public function testKnownExcessiveSizedBodyIsDisgardedTheRequestIsPassedDownToTh ); $buffer = new RequestBodyBufferMiddleware(1); - $response = Block\await($buffer( + $response = \React\Async\await($buffer( $serverRequest, function (ServerRequestInterface $request) { return new Response(200, array(), $request->getBody()->getContents()); @@ -153,7 +152,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 +205,7 @@ function (ServerRequestInterface $request) { $stream->end('aa'); - $exposedResponse = Block\await($promise->then( + $exposedResponse = \React\Async\await($promise->then( null, $this->expectCallableNever() )); @@ -236,7 +235,7 @@ function (ServerRequestInterface $request) { $stream->emit('error', array(new \RuntimeException())); $this->setExpectedException('RuntimeException'); - Block\await($promise); + \React\Async\await($promise); } public function testFullBodyStreamedBeforeCallingNextMiddleware() From 663c9a3b77b71463fa7fcb76a6676ffd16979dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 16 Aug 2022 18:30:57 +0200 Subject: [PATCH 099/152] Do not decode cookie names anymore --- README.md | 4 ++-- examples/55-server-cookie-handling.php | 4 ++-- src/Message/ServerRequest.php | 2 +- tests/Message/ServerRequestTest.php | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ba8d8330..1070349c 100644 --- a/README.md +++ b/README.md @@ -1304,7 +1304,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"; @@ -1316,7 +1316,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!')); }); ``` 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/src/Message/ServerRequest.php b/src/Message/ServerRequest.php index f446f24e..fdb3ec5e 100644 --- a/src/Message/ServerRequest.php +++ b/src/Message/ServerRequest.php @@ -186,7 +186,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; } diff --git a/tests/Message/ServerRequestTest.php b/tests/Message/ServerRequestTest.php index 37cc1879..a5919f64 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() From d92e564a80e349661abba0d9d80a9a82bf120d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 16 Jun 2022 22:16:25 +0200 Subject: [PATCH 100/152] Improve performance, reuse server params for same connection --- src/Io/RequestHeaderParser.php | 84 +++++++++++++++++----------- tests/Io/RequestHeaderParserTest.php | 58 ++++++++++++++++++- 2 files changed, 108 insertions(+), 34 deletions(-) diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 6930afaf..b8336f5b 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -27,6 +27,9 @@ class RequestHeaderParser extends EventEmitter /** @var Clock */ private $clock; + /** @var array> */ + private $connectionParams = array(); + public function __construct(Clock $clock) { $this->clock = $clock; @@ -66,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 = ''; @@ -119,13 +121,12 @@ 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 @@ -160,26 +161,59 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) } } + // 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' => (int) ($now = $this->clock->now()), - 'REQUEST_TIME_FLOAT' => $now - ); + $serverParams['REQUEST_TIME'] = (int) ($now = $this->clock->now()); + $serverParams['REQUEST_TIME_FLOAT'] = $now; // 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://'; - } + $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($localParts['host'], $localParts['port']) ? $localParts['host'] . ':' . $localParts['port'] : '127.0.0.1'; + $host = isset($serverParams['SERVER_ADDR'], $serverParams['SERVER_PORT']) ? $serverParams['SERVER_ADDR'] . ':' . $serverParams['SERVER_PORT'] : '127.0.0.1'; } if ($start['method'] === 'OPTIONS' && $start['target'] === '*') { @@ -210,22 +244,6 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) } } - // 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, diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index 7ba7fe01..87d6bf1b 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -2,9 +2,9 @@ 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 { @@ -808,6 +808,62 @@ 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; From f0b4859d9f1728e6df3877b40956f0e3afef2d34 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sun, 14 Aug 2022 00:29:58 +0200 Subject: [PATCH 101/152] Test on PHP 8.2 With PHP 8.2 coming out later this year, we should be reading for it's release to ensure all out code works on it. Refs: https://github.com/reactphp/event-loop/pull/258 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27577838..0724232c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: php: + - 8.2 - 8.1 - 8.0 - 7.4 From 4a1e85382e8c2a9e0fdb8ac04e94585da2083bfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 23 Aug 2022 14:31:28 +0200 Subject: [PATCH 102/152] Prepare v1.7.0 release --- CHANGELOG.md | 26 +++++++++++++++++++++++++- README.md | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41079cdb..5bf17e9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## 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 +35,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 1070349c..659855b0 100644 --- a/README.md +++ b/README.md @@ -2924,7 +2924,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.7 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 8ec53f525b7fcfe66cc14007415346458d63f1f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 12 Jun 2022 18:56:25 +0200 Subject: [PATCH 103/152] Forward compatibility with upcoming Promise v3 --- .github/workflows/ci.yml | 3 ++ composer.json | 28 ++++++++++++++----- src/Io/StreamingServer.php | 3 +- .../LimitConcurrentRequestsMiddleware.php | 2 +- tests/FunctionalHttpServerTest.php | 4 +++ tests/Io/MiddlewareRunnerTest.php | 3 +- tests/Io/TransactionTest.php | 14 +++++++--- .../LimitConcurrentRequestsMiddlewareTest.php | 8 +++--- 8 files changed, 45 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0724232c..fafa0ff0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,8 @@ jobs: with: php-version: ${{ matrix.php }} coverage: xdebug + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: composer install - run: vendor/bin/phpunit --coverage-text if: ${{ matrix.php >= 7.3 }} @@ -39,6 +41,7 @@ jobs: name: PHPUnit (HHVM) runs-on: ubuntu-18.04 continue-on-error: true + if: false # temporarily skipped until https://github.com/azjezz/setup-hhvm/issues/3 is addressed steps: - uses: actions/checkout@v2 - uses: azjezz/setup-hhvm@v1 diff --git a/composer.json b/composer.json index 57adfc2c..3ba8a606 100644 --- a/composer.json +++ b/composer.json @@ -31,16 +31,16 @@ "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/promise": "^3@dev || ^2.3 || ^1.2.1", + "react/promise-stream": "^1.4", + "react/socket": "^1.12", "react/stream": "^1.2", "ringcentral/psr7": "^1.2" }, "require-dev": { - "clue/http-proxy-react": "^1.7", - "clue/reactphp-ssh-proxy": "^1.3", - "clue/socks-react": "^1.3", + "clue/http-proxy-react": "dev-promise-v3 as 1.8.0", + "clue/reactphp-ssh-proxy": "dev-promise-v3 as 1.4.0", + "clue/socks-react": "dev-promise-v3 as 1.4.0", "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", "react/async": "^4 || ^3 || ^2", "react/promise-timer": "^1.9" @@ -50,5 +50,19 @@ }, "autoload-dev": { "psr-4": { "React\\Tests\\Http\\": "tests" } - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/clue-labs/reactphp-http-proxy" + }, + { + "type": "vcs", + "url": "https://github.com/clue-labs/reactphp-socks" + }, + { + "type": "vcs", + "url": "https://github.com/clue-labs/reactphp-ssh-proxy" + } + ] } diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index a054be3d..13f0b0c4 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; @@ -158,7 +157,7 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface } // cancel pending promise once connection closes - if ($response instanceof CancellablePromiseInterface) { + if ($response instanceof PromiseInterface && \method_exists($response, 'cancel')) { $conn->on('close', function () use ($response) { $response->cancel(); }); 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/tests/FunctionalHttpServerTest.php b/tests/FunctionalHttpServerTest.php index eb3b448c..dcd79b3e 100644 --- a/tests/FunctionalHttpServerTest.php +++ b/tests/FunctionalHttpServerTest.php @@ -726,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( diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index 1f49facd..ac836f03 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -8,7 +8,6 @@ use React\Http\Io\MiddlewareRunner; 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; @@ -479,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/TransactionTest.php b/tests/Io/TransactionTest.php index 83d218c7..d9ac2178 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -436,11 +436,14 @@ 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'); @@ -778,13 +781,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/Middleware/LimitConcurrentRequestsMiddlewareTest.php b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php index 7e537391..6c63a94f 100644 --- a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php +++ b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php @@ -79,7 +79,7 @@ public function testLimitOneRequestConcurrently() /** * Ensure resolve frees up a slot */ - $deferredA->resolve(); + $deferredA->resolve(null); $this->assertTrue($calledA); $this->assertTrue($calledB); @@ -88,7 +88,7 @@ public function testLimitOneRequestConcurrently() /** * Ensure reject also frees up a slot */ - $deferredB->reject(); + $deferredB->reject(new \RuntimeException()); $this->assertTrue($calledA); $this->assertTrue($calledB); @@ -194,7 +194,7 @@ public function testStreamDoesPauseAndThenResumeWhenDequeued() $limitHandlers(new ServerRequest('GET', 'https://example.com/', array(), $body), function () {}); - $deferred->reject(); + $deferred->reject(new \RuntimeException()); } public function testReceivesBufferedRequestSameInstance() @@ -452,7 +452,7 @@ public function testReceivesStreamingBodyChangesInstanceWithCustomBodyButSameDat $req = $request; }); - $deferred->reject(); + $deferred->reject(new \RuntimeException()); $this->assertNotSame($request, $req); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $req); From c556187f9ad466a241212adcefdcc4fa345ac2b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 2 Sep 2022 16:19:03 +0200 Subject: [PATCH 104/152] Update to stable dev dependencies --- .github/workflows/ci.yml | 3 --- composer.json | 24 +++++------------------- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fafa0ff0..0724232c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,8 +29,6 @@ jobs: with: php-version: ${{ matrix.php }} coverage: xdebug - env: - COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: composer install - run: vendor/bin/phpunit --coverage-text if: ${{ matrix.php >= 7.3 }} @@ -41,7 +39,6 @@ jobs: name: PHPUnit (HHVM) runs-on: ubuntu-18.04 continue-on-error: true - if: false # temporarily skipped until https://github.com/azjezz/setup-hhvm/issues/3 is addressed steps: - uses: actions/checkout@v2 - uses: azjezz/setup-hhvm@v1 diff --git a/composer.json b/composer.json index 3ba8a606..d92ac820 100644 --- a/composer.json +++ b/composer.json @@ -31,16 +31,16 @@ "fig/http-message-util": "^1.1", "psr/http-message": "^1.0", "react/event-loop": "^1.2", - "react/promise": "^3@dev || ^2.3 || ^1.2.1", + "react/promise": "^3 || ^2.3 || ^1.2.1", "react/promise-stream": "^1.4", "react/socket": "^1.12", "react/stream": "^1.2", "ringcentral/psr7": "^1.2" }, "require-dev": { - "clue/http-proxy-react": "dev-promise-v3 as 1.8.0", - "clue/reactphp-ssh-proxy": "dev-promise-v3 as 1.4.0", - "clue/socks-react": "dev-promise-v3 as 1.4.0", + "clue/http-proxy-react": "^1.8", + "clue/reactphp-ssh-proxy": "^1.4", + "clue/socks-react": "^1.4", "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", "react/async": "^4 || ^3 || ^2", "react/promise-timer": "^1.9" @@ -50,19 +50,5 @@ }, "autoload-dev": { "psr-4": { "React\\Tests\\Http\\": "tests" } - }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/clue-labs/reactphp-http-proxy" - }, - { - "type": "vcs", - "url": "https://github.com/clue-labs/reactphp-socks" - }, - { - "type": "vcs", - "url": "https://github.com/clue-labs/reactphp-ssh-proxy" - } - ] + } } From 0c27d679a64231343563bfb879c8fe89164093cd Mon Sep 17 00:00:00 2001 From: 51imyyy Date: Mon, 12 Sep 2022 09:52:39 +0200 Subject: [PATCH 105/152] added support for default headers in Browser PHP and moved default header user-agent to the default headers. --- README.md | 32 +++++++++++ src/Browser.php | 72 ++++++++++++++++++++++++ src/Client/RequestData.php | 1 - tests/BrowserTest.php | 97 ++++++++++++++++++++++++++++++++ tests/Client/RequestDataTest.php | 8 --- tests/Client/RequestTest.php | 10 ++-- 6 files changed, 206 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index d3a93ceb..0cdbe74e 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,8 @@ multiple concurrent HTTP requests without blocking. * [withBase()](#withbase) * [withProtocolVersion()](#withprotocolversion) * [withResponseBuffer()](#withresponsebuffer) + * [withHeader()](#withheader) + * [withoutHeader()](#withoutheader) * [React\Http\Message](#reacthttpmessage) * [Response](#response) * [html()](#html) @@ -2381,6 +2383,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 diff --git a/src/Browser.php b/src/Browser.php index 72847f66..16c98fb3 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -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 @@ -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: * @@ -783,6 +842,19 @@ private function requestMayBeStreaming($method, $url, array $headers = array(), $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( new Request($method, $url, $headers, $body, $this->protocolVersion) ); diff --git a/src/Client/RequestData.php b/src/Client/RequestData.php index a5908a08..04bb4cad 100644 --- a/src/Client/RequestData.php +++ b/src/Client/RequestData.php @@ -29,7 +29,6 @@ private function mergeDefaultheaders(array $headers) $defaults = array_merge( array( 'Host' => $this->getHost().$port, - 'User-Agent' => 'ReactPHP/1', ), $connectionHeaders, $authHeaders diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index 39be453a..75717169 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -503,4 +503,101 @@ 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 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/RequestDataTest.php b/tests/Client/RequestDataTest.php index 7f96e152..f6713e85 100644 --- a/tests/Client/RequestDataTest.php +++ b/tests/Client/RequestDataTest.php @@ -14,7 +14,6 @@ public function toStringReturnsHTTPRequestMessage() $expected = "GET / HTTP/1.0\r\n" . "Host: www.example.com\r\n" . - "User-Agent: ReactPHP/1\r\n" . "\r\n"; $this->assertSame($expected, $requestData->__toString()); @@ -27,7 +26,6 @@ public function toStringReturnsHTTPRequestMessageWithEmptyQueryString() $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()); @@ -40,7 +38,6 @@ public function toStringReturnsHTTPRequestMessageWithZeroQueryStringAndRootPath( $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()); @@ -53,7 +50,6 @@ public function toStringReturnsHTTPRequestMessageWithOptionsAbsoluteRequestForm( $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()); @@ -66,7 +62,6 @@ public function toStringReturnsHTTPRequestMessageWithOptionsAsteriskRequestForm( $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()); @@ -80,7 +75,6 @@ public function toStringReturnsHTTPRequestMessageWithProtocolVersion() $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"; @@ -131,7 +125,6 @@ public function toStringReturnsHTTPRequestMessageWithProtocolVersionThroughConst $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"; @@ -145,7 +138,6 @@ public function toStringUsesUserPassFromURL() $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"; diff --git a/tests/Client/RequestTest.php b/tests/Client/RequestTest.php index fb2dc884..cdb209cf 100644 --- a/tests/Client/RequestTest.php +++ b/tests/Client/RequestTest.php @@ -181,7 +181,7 @@ public function postRequestShouldSendAPostRequest() $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$#")); + ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome post data$#")); $request->end('some post data'); @@ -199,7 +199,7 @@ public function writeWithAPostRequestShouldSendToTheStream() $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->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")), array($this->identicalTo("post")), array($this->identicalTo("data")) ); @@ -222,7 +222,7 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent $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->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsomepost$#")), array($this->identicalTo("data")) )->willReturn( true @@ -258,7 +258,7 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB $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->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsomepost$#")), array($this->identicalTo("data")) )->willReturn( false @@ -290,7 +290,7 @@ public function pipeShouldPipeDataIntoTheRequestBody() $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->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")), array($this->identicalTo("post")), array($this->identicalTo("data")) ); From 2290723e79dcd0fa17d8c9432727d3e17dde37b0 Mon Sep 17 00:00:00 2001 From: Fabian Meyer Date: Mon, 19 Sep 2022 11:24:47 +0200 Subject: [PATCH 106/152] Preserve method on redirect --- README.md | 7 ++- src/Io/Transaction.php | 33 ++++++---- tests/Io/TransactionTest.php | 118 ++++++++++++++++++++++++++++++++++- 3 files changed, 143 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 66ea34a8..f1f6d7cd 100644 --- a/README.md +++ b/README.md @@ -342,9 +342,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 diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index 330ffed0..b64622a8 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -5,6 +5,7 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; +use React\Http\Message\Response; use RingCentral\Psr7\Request; use RingCentral\Psr7\Uri; use React\EventLoop\LoopInterface; @@ -234,6 +235,8 @@ public function onResponse(ResponseInterface $response, RequestInterface $reques /** * @param ResponseInterface $response * @param RequestInterface $request + * @param Deferred $deferred + * @param ClientRequestState $state * @return PromiseInterface * @throws \RuntimeException */ @@ -242,7 +245,7 @@ private function onResponseRedirect(ResponseInterface $response, RequestInterfac // resolve location relative to last request URI $location = Uri::resolve($request->getUri(), $response->getHeaderLine('Location')); - $request = $this->makeRedirectRequest($request, $location); + $request = $this->makeRedirectRequest($request, $location, $response->getStatusCode()); $this->progress('redirect', array($request)); if ($state->numRequests >= $this->maxRedirects) { @@ -255,25 +258,33 @@ private function onResponseRedirect(ResponseInterface $response, RequestInterfac /** * @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 EmptyBodyStream()); + } - return new Request($method, $location, $request->getHeaders()); + return $request; } private function progress($name, array $args = array()) diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index d9ac2178..05022009 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -663,7 +663,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), @@ -674,6 +674,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(); From aa7512ee17258c88466de30f9cb44ec5f9df3ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 29 Sep 2022 14:55:52 +0200 Subject: [PATCH 107/152] Prepare v1.8.0 release --- CHANGELOG.md | 17 ++++++++++++++++- README.md | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bf17e9f..00e2d07e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 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. @@ -19,7 +34,7 @@ This is a **SECURITY** and feature release for the 1.x series of ReactPHP's HTTP (#444 by @mrsimonbennett) * Minor documentation improvements. - (#452 by @clue, #458 by @nhedger, #448 by @jorrit and #446 by @SimonFrings + (#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. diff --git a/README.md b/README.md index 66ea34a8..1df8873f 100644 --- a/README.md +++ b/README.md @@ -2956,7 +2956,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -composer require react/http:^1.7 +composer require react/http:^1.8 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From f2a1446f0d735d2ae2eb5faaa08bc2725e6a9c9d Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Tue, 6 Sep 2022 16:13:29 +0200 Subject: [PATCH 108/152] Add issue template for better orientation --- .github/ISSUE_TEMPLATE/bug.md | 11 +++++++++++ .github/ISSUE_TEMPLATE/config.yml | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 00000000..d26fe152 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,11 @@ +--- +name: Bug report +about: Found a bug in our project? Create a report to help us improve. +labels: bug +--- + + + +```php +// Please add code examples if possible, so we can reproduce your steps +``` diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..4b4a0ea6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Report a security vulnerability + url: https://reactphp.org/#support + about: 'If you discover a security vulnerability, please send us an email. Do not disclose security-related issues publicly.' + - name: Feature request + url: https://github.com/orgs/reactphp/discussions/categories/ideas + about: 'You have ideas to improve our project? Start a new discussion in our "Ideas" category.' + - name: Questions + url: https://github.com/orgs/reactphp/discussions/categories/q-a + about: 'We are happy to answer your questions! Start a new discussion in our "Q&A" category.' From 44f0a80f7a1616249cc6c817d30921993d5776cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 12 Nov 2022 17:19:44 +0100 Subject: [PATCH 109/152] Update test suite and report failed assertions --- .github/workflows/ci.yml | 22 +++++++++++++--------- composer.json | 10 +++++++--- phpunit.xml.dist | 14 +++++++++++--- phpunit.xml.legacy | 10 +++++++++- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0724232c..55bbaa5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: jobs: PHPUnit: name: PHPUnit (PHP ${{ matrix.php }}) - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: php: @@ -24,11 +24,12 @@ jobs: - 5.4 - 5.3 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - 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 }} @@ -37,13 +38,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@v3 + - 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: composer self-update --2.2 # downgrade Composer for HHVM - - 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/composer.json b/composer.json index d92ac820..aeee592b 100644 --- a/composer.json +++ b/composer.json @@ -41,14 +41,18 @@ "clue/http-proxy-react": "^1.8", "clue/reactphp-ssh-proxy": "^1.4", "clue/socks-react": "^1.4", - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", "react/async": "^4 || ^3 || ^2", "react/promise-timer": "^1.9" }, "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/phpunit.xml.dist b/phpunit.xml.dist index 93a36f6b..7a9577e9 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..ac5600ae 100644 --- a/phpunit.xml.legacy +++ b/phpunit.xml.legacy @@ -1,6 +1,6 @@ - + ./src/ + + + + + + + + From b8f6efa7225e3da486606ca757554174b8f40ebe Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Thu, 17 Nov 2022 15:27:56 +0100 Subject: [PATCH 110/152] Revert issue template changes to use organisation issue template --- .github/ISSUE_TEMPLATE/bug.md | 11 ----------- .github/ISSUE_TEMPLATE/config.yml | 11 ----------- 2 files changed, 22 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug.md delete mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md deleted file mode 100644 index d26fe152..00000000 --- a/.github/ISSUE_TEMPLATE/bug.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Bug report -about: Found a bug in our project? Create a report to help us improve. -labels: bug ---- - - - -```php -// Please add code examples if possible, so we can reproduce your steps -``` diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 4b4a0ea6..00000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,11 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: Report a security vulnerability - url: https://reactphp.org/#support - about: 'If you discover a security vulnerability, please send us an email. Do not disclose security-related issues publicly.' - - name: Feature request - url: https://github.com/orgs/reactphp/discussions/categories/ideas - about: 'You have ideas to improve our project? Start a new discussion in our "Ideas" category.' - - name: Questions - url: https://github.com/orgs/reactphp/discussions/categories/q-a - about: 'We are happy to answer your questions! Start a new discussion in our "Q&A" category.' From 7a27c49ec600940ab062d7f378a7fed7d3fb54ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 22 Nov 2022 14:40:33 +0100 Subject: [PATCH 111/152] Add `Request` class to represent outgoing HTTP request message --- README.md | 19 ++++++++++ src/Browser.php | 7 +--- src/Io/Transaction.php | 5 ++- src/Message/Request.php | 58 ++++++++++++++++++++++++++++++ src/Message/ServerRequest.php | 4 +-- tests/FunctionalBrowserTest.php | 3 +- tests/Io/SenderTest.php | 2 +- tests/Io/TransactionTest.php | 4 +-- tests/Message/RequestTest.php | 63 +++++++++++++++++++++++++++++++++ 9 files changed, 149 insertions(+), 16 deletions(-) create mode 100644 src/Message/Request.php create mode 100644 tests/Message/RequestTest.php diff --git a/README.md b/README.md index 55ebb8e7..271f5e87 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ multiple concurrent HTTP requests without blocking. * [json()](#json) * [plaintext()](#plaintext) * [xml()](#xml) + * [Request](#request-1) * [ServerRequest](#serverrequest) * [ResponseException](#responseexception) * [React\Http\Middleware](#reacthttpmiddleware) @@ -2628,6 +2629,24 @@ $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 an existing outgoing + request message and only adds support for streaming. This base class is + considered an implementation detail that may change in the future. + #### ServerRequest The `React\Http\Message\ServerRequest` class can be used to diff --git a/src/Browser.php b/src/Browser.php index 16c98fb3..3e3458af 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -3,13 +3,12 @@ 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\Promise\PromiseInterface; use React\Socket\ConnectorInterface; use React\Stream\ReadableStreamInterface; @@ -838,10 +837,6 @@ private function requestMayBeStreaming($method, $url, array $headers = array(), $url = Uri::resolve($this->baseUrl, $url); } - if ($body instanceof ReadableStreamInterface) { - $body = new ReadableBodyStream($body); - } - foreach ($this->defaultHeaders as $key => $value) { $explicitHeaderExists = false; foreach (\array_keys($headers) as $headerKey) { diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index b64622a8..cbf8f3eb 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -5,14 +5,13 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; -use React\Http\Message\Response; -use RingCentral\Psr7\Request; -use RingCentral\Psr7\Uri; use React\EventLoop\LoopInterface; +use React\Http\Message\Response; use React\Http\Message\ResponseException; use React\Promise\Deferred; use React\Promise\PromiseInterface; use React\Stream\ReadableStreamInterface; +use RingCentral\Psr7\Uri; /** * @internal diff --git a/src/Message/Request.php b/src/Message/Request.php new file mode 100644 index 00000000..cf59641e --- /dev/null +++ b/src/Message/Request.php @@ -0,0 +1,58 @@ + Internally, this implementation builds on top of an existing outgoing + * request message and only adds support for streaming. This base class is + * considered an implementation detail that may change in the future. + * + * @see RequestInterface + */ +final class Request extends BaseRequest 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/ServerRequest.php b/src/Message/ServerRequest.php index fdb3ec5e..25532cf4 100644 --- a/src/Message/ServerRequest.php +++ b/src/Message/ServerRequest.php @@ -8,7 +8,7 @@ use React\Http\Io\BufferedBody; use React\Http\Io\HttpBodyStream; use React\Stream\ReadableStreamInterface; -use RingCentral\Psr7\Request; +use RingCentral\Psr7\Request as BaseRequest; /** * Respresents an incoming server request message. @@ -30,7 +30,7 @@ * * @see ServerRequestInterface */ -final class ServerRequest extends Request implements ServerRequestInterface +final class ServerRequest extends BaseRequest implements ServerRequestInterface { private $attributes = array(); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 95092ac1..6def2ecc 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -7,16 +7,15 @@ 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 { diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 587ba0c2..91b87b30 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -6,10 +6,10 @@ use React\Http\Client\RequestData; 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 { diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 05022009..e0d04e39 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -7,14 +7,14 @@ 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 RingCentral\Psr7\Response; class TransactionTest extends TestCase { 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 + ); + } +} From 01228fa89454b00695f68c84e177b400715ff081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 23 Nov 2022 14:06:26 +0100 Subject: [PATCH 112/152] Rename internal `Request` to `ClientRequestStream` --- src/Client/Client.php | 6 ++- .../ClientRequestStream.php} | 7 +-- .../ClientRequestStreamTest.php} | 48 +++++++++---------- tests/Io/SenderTest.php | 26 +++++----- 4 files changed, 45 insertions(+), 42 deletions(-) rename src/{Client/Request.php => Io/ClientRequestStream.php} (96%) rename tests/{Client/RequestTest.php => Io/ClientRequestStreamTest.php} (89%) diff --git a/src/Client/Client.php b/src/Client/Client.php index 7a97349c..62caed5f 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -3,8 +3,9 @@ namespace React\Http\Client; use React\EventLoop\LoopInterface; -use React\Socket\ConnectorInterface; +use React\Http\Io\ClientRequestStream; use React\Socket\Connector; +use React\Socket\ConnectorInterface; /** * @internal @@ -22,10 +23,11 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector = $this->connector = $connector; } + /** @return ClientRequestStream */ public function request($method, $url, array $headers = array(), $protocolVersion = '1.0') { $requestData = new RequestData($method, $url, $headers, $protocolVersion); - return new Request($this->connector, $requestData); + return new ClientRequestStream($this->connector, $requestData); } } diff --git a/src/Client/Request.php b/src/Io/ClientRequestStream.php similarity index 96% rename from src/Client/Request.php rename to src/Io/ClientRequestStream.php index 51e03313..2513a89a 100644 --- a/src/Client/Request.php +++ b/src/Io/ClientRequestStream.php @@ -1,8 +1,9 @@ write($headers . $pendingWrites); - $stateRef = Request::STATE_HEAD_WRITTEN; + $stateRef = ClientRequestStream::STATE_HEAD_WRITTEN; // clear pending writes if non-empty if ($pendingWrites !== '') { diff --git a/tests/Client/RequestTest.php b/tests/Io/ClientRequestStreamTest.php similarity index 89% rename from tests/Client/RequestTest.php rename to tests/Io/ClientRequestStreamTest.php index cdb209cf..6e3e16b8 100644 --- a/tests/Client/RequestTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -1,16 +1,16 @@ connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -67,7 +67,7 @@ public function requestShouldBindToStreamEventsAndUseconnector() public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme() { $requestData = new RequestData('GET', 'https://www.example.com/'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once())->method('connect')->with('tls://www.example.com:443')->willReturn(new Promise(function () { })); @@ -78,7 +78,7 @@ public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme() public function requestShouldEmitErrorIfConnectionFails() { $requestData = new RequestData('GET', 'http://www.example.com/'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException())); @@ -94,7 +94,7 @@ public function requestShouldEmitErrorIfConnectionFails() public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() { $requestData = new RequestData('GET', 'http://www.example.com/'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -111,7 +111,7 @@ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() public function requestShouldEmitErrorIfConnectionEmitsError() { $requestData = new RequestData('GET', 'http://www.example.com/'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -128,7 +128,7 @@ public function requestShouldEmitErrorIfConnectionEmitsError() public function requestShouldEmitErrorIfRequestParserThrowsException() { $requestData = new RequestData('GET', 'http://www.example.com/'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -144,7 +144,7 @@ public function requestShouldEmitErrorIfRequestParserThrowsException() public function requestShouldEmitErrorIfUrlIsInvalid() { $requestData = new RequestData('GET', 'ftp://www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); @@ -160,7 +160,7 @@ public function requestShouldEmitErrorIfUrlIsInvalid() public function requestShouldEmitErrorIfUrlHasNoScheme() { $requestData = new RequestData('GET', 'www.example.com'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); @@ -174,7 +174,7 @@ public function requestShouldEmitErrorIfUrlHasNoScheme() public function postRequestShouldSendAPostRequest() { $requestData = new RequestData('POST', 'http://www.example.com/'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -194,7 +194,7 @@ public function postRequestShouldSendAPostRequest() public function writeWithAPostRequestShouldSendToTheStream() { $requestData = new RequestData('POST', 'http://www.example.com/'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -217,7 +217,7 @@ public function writeWithAPostRequestShouldSendToTheStream() public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent() { $requestData = new RequestData('POST', 'http://www.example.com/'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $resolveConnection = $this->successfulAsyncConnectionMock(); @@ -248,7 +248,7 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsBuffer() { $requestData = new RequestData('POST', 'http://www.example.com/'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->stream = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() @@ -285,7 +285,7 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB public function pipeShouldPipeDataIntoTheRequestBody() { $requestData = new RequestData('POST', 'http://www.example.com/'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -318,7 +318,7 @@ public function pipeShouldPipeDataIntoTheRequestBody() public function writeShouldStartConnecting() { $requestData = new RequestData('POST', 'http://www.example.com/'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once()) ->method('connect') @@ -334,7 +334,7 @@ public function writeShouldStartConnecting() public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() { $requestData = new RequestData('POST', 'http://www.example.com/'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once()) ->method('connect') @@ -352,7 +352,7 @@ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() public function closeShouldEmitCloseEvent() { $requestData = new RequestData('POST', 'http://www.example.com/'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $request->on('close', $this->expectCallableOnce()); $request->close(); @@ -364,7 +364,7 @@ public function closeShouldEmitCloseEvent() public function writeAfterCloseReturnsFalse() { $requestData = new RequestData('POST', 'http://www.example.com/'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $request->close(); @@ -378,7 +378,7 @@ public function writeAfterCloseReturnsFalse() public function endAfterCloseIsNoOp() { $requestData = new RequestData('POST', 'http://www.example.com/'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->never()) ->method('connect'); @@ -393,7 +393,7 @@ public function endAfterCloseIsNoOp() public function closeShouldCancelPendingConnectionAttempt() { $requestData = new RequestData('POST', 'http://www.example.com/'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $promise = new Promise(function () {}, function () { throw new \RuntimeException(); @@ -417,7 +417,7 @@ public function closeShouldCancelPendingConnectionAttempt() public function requestShouldRemoveAllListenerAfterClosed() { $requestData = new RequestData('GET', 'http://www.example.com/'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $request->on('close', function () {}); $this->assertCount(1, $request->listeners('close')); @@ -451,7 +451,7 @@ private function successfulAsyncConnectionMock() public function multivalueHeader() { $requestData = new RequestData('GET', 'http://www.example.com/'); - $request = new Request($this->connector, $requestData); + $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 91b87b30..6d8c3b5f 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -76,7 +76,7 @@ public function testSendPostWillAutomaticallySendContentLengthHeader() 'http://www.google.com/', array('Host' => 'www.google.com', 'Content-Length' => '5'), '1.1' - )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -92,7 +92,7 @@ public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmpty 'http://www.google.com/', array('Host' => 'www.google.com', 'Content-Length' => '0'), '1.1' - )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -102,7 +102,7 @@ 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(); @@ -122,7 +122,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); @@ -141,7 +141,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); @@ -160,7 +160,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'); @@ -190,7 +190,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'); @@ -218,7 +218,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'); @@ -252,7 +252,7 @@ public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() 'http://www.google.com/', array('Host' => 'www.google.com', 'Content-Length' => '100'), '1.1' - )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -269,7 +269,7 @@ public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() 'http://www.google.com/', array('Host' => 'www.google.com'), '1.1' - )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -285,7 +285,7 @@ public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyReques 'http://www.google.com/', array('Host' => 'www.google.com'), '1.1' - )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -301,7 +301,7 @@ public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsI 'http://www.google.com/', array('Host' => 'www.google.com', 'Content-Length' => '0'), '1.1' - )->willReturn($this->getMockBuilder('React\Http\Client\Request')->disableOriginalConstructor()->getMock()); + )->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); @@ -393,7 +393,7 @@ public function testRequestProtocolVersion(Request $Request, $method, $uri, $hea $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(), ))->getMock(); - $request = $this->getMockBuilder('React\Http\Client\Request') + $request = $this->getMockBuilder('React\Http\Io\ClientRequestStream') ->setMethods(array()) ->setConstructorArgs(array( $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(), From 212a3bba511f307eb4efb311e5afafb68b3ecd05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 24 Nov 2022 16:45:11 +0100 Subject: [PATCH 113/152] Refactor to remove internal `RequestData` --- src/Client/Client.php | 7 +- src/Client/RequestData.php | 127 -------------- src/Io/ClientRequestStream.php | 39 +++-- src/Io/Sender.php | 16 +- tests/Client/FunctionalIntegrationTest.php | 13 +- tests/Client/RequestDataTest.php | 146 ---------------- tests/Io/ClientRequestStreamTest.php | 87 +++++++--- tests/Io/SenderTest.php | 185 ++++++++++----------- 8 files changed, 205 insertions(+), 415 deletions(-) delete mode 100644 src/Client/RequestData.php delete mode 100644 tests/Client/RequestDataTest.php diff --git a/src/Client/Client.php b/src/Client/Client.php index 62caed5f..c3fd4570 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -2,6 +2,7 @@ namespace React\Http\Client; +use Psr\Http\Message\RequestInterface; use React\EventLoop\LoopInterface; use React\Http\Io\ClientRequestStream; use React\Socket\Connector; @@ -24,10 +25,8 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector = } /** @return ClientRequestStream */ - public function request($method, $url, array $headers = array(), $protocolVersion = '1.0') + public function request(RequestInterface $request) { - $requestData = new RequestData($method, $url, $headers, $protocolVersion); - - return new ClientRequestStream($this->connector, $requestData); + return new ClientRequestStream($this->connector, $request); } } diff --git a/src/Client/RequestData.php b/src/Client/RequestData.php deleted file mode 100644 index 04bb4cad..00000000 --- a/src/Client/RequestData.php +++ /dev/null @@ -1,127 +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, - ), - $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/ClientRequestStream.php b/src/Io/ClientRequestStream.php index 2513a89a..29536e88 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -3,7 +3,7 @@ namespace React\Http\Io; use Evenement\EventEmitter; -use React\Http\Client\RequestData; +use Psr\Http\Message\RequestInterface; use React\Promise; use React\Socket\ConnectionInterface; use React\Socket\ConnectorInterface; @@ -24,10 +24,15 @@ class ClientRequestStream extends EventEmitter implements WritableStreamInterfac const STATE_HEAD_WRITTEN = 2; const STATE_END = 3; + /** @var ConnectorInterface */ private $connector; - private $requestData; + /** @var RequestInterface */ + private $request; + + /** @var ?ConnectionInterface */ private $stream; + private $buffer; private $responseFactory; private $state = self::STATE_INIT; @@ -35,10 +40,10 @@ class ClientRequestStream extends EventEmitter implements WritableStreamInterfac private $pendingWrites = ''; - public function __construct(ConnectorInterface $connector, RequestData $requestData) + public function __construct(ConnectorInterface $connector, RequestInterface $request) { $this->connector = $connector; - $this->requestData = $requestData; + $this->request = $request; } public function isWritable() @@ -50,7 +55,7 @@ private function writeHead() { $this->state = self::STATE_WRITING_HEAD; - $requestData = $this->requestData; + $request = $this->request; $streamRef = &$this->stream; $stateRef = &$this->state; $pendingWrites = &$this->pendingWrites; @@ -58,8 +63,9 @@ private function writeHead() $promise = $this->connect(); $promise->then( - function (ConnectionInterface $stream) use ($requestData, &$streamRef, &$stateRef, &$pendingWrites, $that) { + function (ConnectionInterface $stream) use ($request, &$streamRef, &$stateRef, &$pendingWrites, $that) { $streamRef = $stream; + assert($streamRef instanceof ConnectionInterface); $stream->on('drain', array($that, 'handleDrain')); $stream->on('data', array($that, 'handleData')); @@ -67,10 +73,17 @@ function (ConnectionInterface $stream) use ($requestData, &$streamRef, &$stateRe $stream->on('error', array($that, 'handleError')); $stream->on('close', array($that, 'handleClose')); - $headers = (string) $requestData; + assert($request instanceof RequestInterface); + $headers = "{$request->getMethod()} {$request->getRequestTarget()} HTTP/{$request->getProtocolVersion()}\r\n"; + foreach ($request->getHeaders() as $name => $values) { + foreach ($values as $value) { + $headers .= "$name: $value\r\n"; + } + } - $more = $stream->write($headers . $pendingWrites); + $more = $stream->write($headers . "\r\n" . $pendingWrites); + assert($stateRef === ClientRequestStream::STATE_WRITING_HEAD); $stateRef = ClientRequestStream::STATE_HEAD_WRITTEN; // clear pending writes if non-empty @@ -218,20 +231,24 @@ public function close() protected function connect() { - $scheme = $this->requestData->getScheme(); + $scheme = $this->request->getUri()->getScheme(); if ($scheme !== 'https' && $scheme !== 'http') { return Promise\reject( new \InvalidArgumentException('Invalid request URL given') ); } - $host = $this->requestData->getHost(); - $port = $this->requestData->getPort(); + $host = $this->request->getUri()->getHost(); + $port = $this->request->getUri()->getPort(); if ($scheme === 'https') { $host = 'tls://' . $host; } + if ($port === null) { + $port = $scheme === 'https' ? 443 : 80; + } + return $this->connector ->connect($host . ':' . $port); } diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 2f04c797..2e821f5a 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -74,6 +74,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(); @@ -91,12 +94,17 @@ public function send(RequestInterface $request) $size = 0; } - $headers = array(); - foreach ($request->getHeaders() as $name => $values) { - $headers[$name] = implode(', ', $values); + // automatically add `Connection: close` request header for HTTP/1.1 requests to avoid connection reuse + if ($request->getProtocolVersion() === '1.1' && !$request->hasHeader('Connection')) { + $request = $request->withHeader('Connection', 'close'); + } + + // 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 diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index d95bf828..90d8444b 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -5,6 +5,7 @@ use Psr\Http\Message\ResponseInterface; use React\EventLoop\Loop; use React\Http\Client\Client; +use React\Http\Message\Request; use React\Promise\Deferred; use React\Promise\Stream; use React\Socket\ConnectionInterface; @@ -45,7 +46,7 @@ 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); + $request = $client->request(new Request('GET', 'http://localhost/' . $port, array(), '', '1.0')); $promise = Stream\first($request, 'close'); $request->end(); @@ -62,7 +63,7 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp }); $client = new Client(Loop::get()); - $request = $client->request('GET', str_replace('tcp:', 'http:', $socket->getAddress())); + $request = $client->request(new Request('GET', str_replace('tcp:', 'http:', $socket->getAddress()), array(), '', '1.0')); $once = $this->expectCallableOnceWith('body'); $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) { @@ -83,7 +84,7 @@ public function testSuccessfulResponseEmitsEnd() $client = new Client(Loop::get()); - $request = $client->request('GET', 'http://www.google.com/'); + $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); $once = $this->expectCallableOnce(); $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) { @@ -109,7 +110,7 @@ public function testPostDataReturnsData() $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))); + $request = $client->request(new Request('POST', 'https://' . (mt_rand(0, 1) === 0 ? 'eu.' : '') . 'httpbin.org/post', array('Content-Length' => strlen($data)), '', '1.0')); $deferred = new Deferred(); $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred) { @@ -141,7 +142,7 @@ public function testPostJsonReturnsData() $client = new Client(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('POST', 'https://httpbin.org/post', array('Content-Length' => strlen($data), 'Content-Type' => 'application/json'), '', '1.0')); $deferred = new Deferred(); $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred) { @@ -170,7 +171,7 @@ public function testCancelPendingConnectionEmitsClose() $client = new Client(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 f6713e85..00000000 --- a/tests/Client/RequestDataTest.php +++ /dev/null @@ -1,146 +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" . - "\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" . - "\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" . - "\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" . - "\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" . - "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" . - "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" . - "Authorization: Basic am9objpkdW1teQ==\r\n" . - "\r\n"; - - $this->assertSame($expected, $requestData->__toString()); - } -} diff --git a/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index 6e3e16b8..07a4eb73 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -2,12 +2,11 @@ namespace React\Tests\Http\Io; -use React\Http\Client\RequestData; use React\Http\Io\ClientRequestStream; -use React\Stream\DuplexResourceStream; -use React\Promise\RejectedPromise; +use React\Http\Message\Request; use React\Promise\Deferred; use React\Promise\Promise; +use React\Stream\DuplexResourceStream; use React\Tests\Http\TestCase; class ClientRequestStreamTest extends TestCase @@ -31,7 +30,7 @@ public function setUpStream() /** @test */ public function requestShouldBindToStreamEventsAndUseconnector() { - $requestData = new RequestData('GET', 'http://www.example.com/'); + $requestData = new Request('GET', 'http://www.example.com/'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -66,7 +65,7 @@ public function requestShouldBindToStreamEventsAndUseconnector() */ public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme() { - $requestData = new RequestData('GET', 'https://www.example.com/'); + $requestData = new Request('GET', 'https://www.example.com/'); $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once())->method('connect')->with('tls://www.example.com:443')->willReturn(new Promise(function () { })); @@ -77,7 +76,7 @@ public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme() /** @test */ public function requestShouldEmitErrorIfConnectionFails() { - $requestData = new RequestData('GET', 'http://www.example.com/'); + $requestData = new Request('GET', 'http://www.example.com/'); $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException())); @@ -93,7 +92,7 @@ public function requestShouldEmitErrorIfConnectionFails() /** @test */ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() { - $requestData = new RequestData('GET', 'http://www.example.com/'); + $requestData = new Request('GET', 'http://www.example.com/'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -110,7 +109,7 @@ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() /** @test */ public function requestShouldEmitErrorIfConnectionEmitsError() { - $requestData = new RequestData('GET', 'http://www.example.com/'); + $requestData = new Request('GET', 'http://www.example.com/'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -127,7 +126,7 @@ public function requestShouldEmitErrorIfConnectionEmitsError() /** @test */ public function requestShouldEmitErrorIfRequestParserThrowsException() { - $requestData = new RequestData('GET', 'http://www.example.com/'); + $requestData = new Request('GET', 'http://www.example.com/'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -143,7 +142,7 @@ public function requestShouldEmitErrorIfRequestParserThrowsException() */ public function requestShouldEmitErrorIfUrlIsInvalid() { - $requestData = new RequestData('GET', 'ftp://www.example.com'); + $requestData = new Request('GET', 'ftp://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); @@ -159,7 +158,7 @@ public function requestShouldEmitErrorIfUrlIsInvalid() */ public function requestShouldEmitErrorIfUrlHasNoScheme() { - $requestData = new RequestData('GET', 'www.example.com'); + $requestData = new Request('GET', 'www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); @@ -170,10 +169,50 @@ public function requestShouldEmitErrorIfUrlHasNoScheme() $request->end(); } + /** @test */ + public function getRequestShouldSendAGetRequest() + { + $requestData = new Request('GET', 'http://www.example.com/', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $this->stream->expects($this->once())->method('write')->with("GET / HTTP/1.0\r\nHost: www.example.com\r\n\r\n"); + + $request->end(); + } + + /** @test */ + public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionCloseHeader() + { + $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $this->stream->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + + $request->end(); + } + + /** @test */ + public function getOptionsAsteriskShouldSendAOptionsRequestAsteriskRequestTarget() + { + $requestData = new Request('OPTIONS', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $requestData = $requestData->withRequestTarget('*'); + $request = new ClientRequestStream($this->connector, $requestData); + + $this->successfulConnectionMock(); + + $this->stream->expects($this->once())->method('write')->with("OPTIONS * HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + + $request->end(); + } + /** @test */ public function postRequestShouldSendAPostRequest() { - $requestData = new RequestData('POST', 'http://www.example.com/'); + $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -193,7 +232,7 @@ public function postRequestShouldSendAPostRequest() /** @test */ public function writeWithAPostRequestShouldSendToTheStream() { - $requestData = new RequestData('POST', 'http://www.example.com/'); + $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -216,7 +255,7 @@ public function writeWithAPostRequestShouldSendToTheStream() /** @test */ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent() { - $requestData = new RequestData('POST', 'http://www.example.com/'); + $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0'); $request = new ClientRequestStream($this->connector, $requestData); $resolveConnection = $this->successfulAsyncConnectionMock(); @@ -247,7 +286,7 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent /** @test */ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsBuffer() { - $requestData = new RequestData('POST', 'http://www.example.com/'); + $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0'); $request = new ClientRequestStream($this->connector, $requestData); $this->stream = $this->getMockBuilder('React\Socket\Connection') @@ -284,7 +323,7 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB /** @test */ public function pipeShouldPipeDataIntoTheRequestBody() { - $requestData = new RequestData('POST', 'http://www.example.com/'); + $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); @@ -317,7 +356,7 @@ public function pipeShouldPipeDataIntoTheRequestBody() */ public function writeShouldStartConnecting() { - $requestData = new RequestData('POST', 'http://www.example.com/'); + $requestData = new Request('POST', 'http://www.example.com/'); $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once()) @@ -333,7 +372,7 @@ public function writeShouldStartConnecting() */ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() { - $requestData = new RequestData('POST', 'http://www.example.com/'); + $requestData = new Request('POST', 'http://www.example.com/'); $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->once()) @@ -351,7 +390,7 @@ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() */ public function closeShouldEmitCloseEvent() { - $requestData = new RequestData('POST', 'http://www.example.com/'); + $requestData = new Request('POST', 'http://www.example.com/'); $request = new ClientRequestStream($this->connector, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -363,7 +402,7 @@ public function closeShouldEmitCloseEvent() */ public function writeAfterCloseReturnsFalse() { - $requestData = new RequestData('POST', 'http://www.example.com/'); + $requestData = new Request('POST', 'http://www.example.com/'); $request = new ClientRequestStream($this->connector, $requestData); $request->close(); @@ -377,7 +416,7 @@ public function writeAfterCloseReturnsFalse() */ public function endAfterCloseIsNoOp() { - $requestData = new RequestData('POST', 'http://www.example.com/'); + $requestData = new Request('POST', 'http://www.example.com/'); $request = new ClientRequestStream($this->connector, $requestData); $this->connector->expects($this->never()) @@ -392,7 +431,7 @@ public function endAfterCloseIsNoOp() */ public function closeShouldCancelPendingConnectionAttempt() { - $requestData = new RequestData('POST', 'http://www.example.com/'); + $requestData = new Request('POST', 'http://www.example.com/'); $request = new ClientRequestStream($this->connector, $requestData); $promise = new Promise(function () {}, function () { @@ -416,7 +455,7 @@ public function closeShouldCancelPendingConnectionAttempt() /** @test */ public function requestShouldRemoveAllListenerAfterClosed() { - $requestData = new RequestData('GET', 'http://www.example.com/'); + $requestData = new Request('GET', 'http://www.example.com/'); $request = new ClientRequestStream($this->connector, $requestData); $request->on('close', function () {}); @@ -450,7 +489,7 @@ private function successfulAsyncConnectionMock() /** @test */ public function multivalueHeader() { - $requestData = new RequestData('GET', 'http://www.example.com/'); + $requestData = new Request('GET', 'http://www.example.com/'); $request = new ClientRequestStream($this->connector, $requestData); $this->successfulConnectionMock(); diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 6d8c3b5f..c2357a1a 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -2,8 +2,8 @@ namespace React\Tests\Http\Io; +use Psr\Http\Message\RequestInterface; use React\Http\Client\Client as HttpClient; -use React\Http\Client\RequestData; use React\Http\Io\ReadableBodyStream; use React\Http\Io\Sender; use React\Http\Message\Request; @@ -71,12 +71,9 @@ public function testSenderConnectorRejection() 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\Io\ClientRequestStream')->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); @@ -87,12 +84,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\Io\ClientRequestStream')->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); @@ -106,12 +100,9 @@ public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() $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); @@ -247,12 +238,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\Io\ClientRequestStream')->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); @@ -264,12 +252,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\Io\ClientRequestStream')->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); @@ -280,12 +265,9 @@ public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() 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\Io\ClientRequestStream')->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); @@ -296,12 +278,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\Io\ClientRequestStream')->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); @@ -309,6 +288,76 @@ public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsI $sender->send($request); } + /** @test */ + public function getHttp10RequestShouldSendAGetRequestWithoutConnectionHeaderByDefault() + { + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return !$request->hasHeader('Connection'); + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client); + + $request = new Request('GET', 'http://www.example.com/', array(), '', '1.0'); + $sender->send($request); + } + + /** @test */ + public function getHttp11RequestShouldSendAGetRequestWithConnectionCloseHeaderByDefault() + { + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Connection') === 'close'; + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client); + + $request = new Request('GET', 'http://www.example.com/', array(), '', '1.1'); + $sender->send($request); + } + + /** @test */ + public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionUpgradeHeader() + { + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return $request->getHeaderLine('Connection') === 'upgrade'; + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client); + + $request = new Request('GET', 'http://www.example.com/', array('Connection' => 'upgrade'), '', '1.1'); + $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 () { @@ -355,54 +404,4 @@ public function testCancelRequestWillCloseConnection() $this->assertInstanceOf('RuntimeException', $exception); } - - 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', - ), - ); - } - - /** - * @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\Io\ClientRequestStream') - ->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); - } } From bafa2afaacc813ff2bef8a9e5066adf72876d617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 3 Sep 2022 11:44:55 +0200 Subject: [PATCH 114/152] Include buffer logic to avoid dependency on reactphp/promise-stream --- composer.json | 2 +- src/Io/Transaction.php | 68 +++++--- .../RequestBodyBufferMiddleware.php | 75 ++++++-- tests/Io/TransactionTest.php | 94 +++++++++- .../RequestBodyBufferMiddlewareTest.php | 164 ++++++++++++++++-- 5 files changed, 346 insertions(+), 57 deletions(-) diff --git a/composer.json b/composer.json index aeee592b..59736ddd 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,6 @@ "psr/http-message": "^1.0", "react/event-loop": "^1.2", "react/promise": "^3 || ^2.3 || ^1.2.1", - "react/promise-stream": "^1.4", "react/socket": "^1.12", "react/stream": "^1.2", "ringcentral/psr7": "^1.2" @@ -43,6 +42,7 @@ "clue/socks-react": "^1.4", "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", "react/async": "^4 || ^3 || ^2", + "react/promise-stream": "^1.4", "react/promise-timer": "^1.9" }, "autoload": { diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index cbf8f3eb..bfa42241 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -9,6 +9,7 @@ use React\Http\Message\Response; use React\Http\Message\ResponseException; use React\Promise\Deferred; +use React\Promise\Promise; use React\Promise\PromiseInterface; use React\Stream\ReadableStreamInterface; use RingCentral\Psr7\Uri; @@ -165,46 +166,67 @@ function (ResponseInterface $response) use ($request, $that, $deferred, $state) */ 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; - } - ); - - $state->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'); + }); } /** 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/Io/TransactionTest.php b/tests/Io/TransactionTest.php index e0d04e39..140c53e0 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -406,7 +406,7 @@ public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefau $this->assertEquals('hello world', (string)$response->getBody()); } - public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBufferWillRejectAndCloseResponseStream() + public function testReceivingStreamingBodyWithContentLengthExceedingMaximumResponseBufferWillRejectAndCloseResponseStreamImmediately() { $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); @@ -419,11 +419,87 @@ public function testReceivingStreamingBodyWithSizeExceedingMaximumResponseBuffer $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->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); - $this->setExpectedException('OverflowException'); - \React\Async\await(\React\Promise\Timer\timeout($promise, 0.001)); + $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() @@ -446,8 +522,16 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() $deferred->resolve($response); $promise->cancel(); - $this->setExpectedException('RuntimeException'); - \React\Async\await(\React\Promise\Timer\timeout($promise, 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() diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index 0edec7da..fd818a8c 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -115,10 +115,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/', @@ -126,13 +127,41 @@ public function testKnownExcessiveSizedBodyIsDisgardedTheRequestIsPassedDownToTh new HttpBodyStream($stream, 2) ); + $exposedRequest = null; $buffer = new RequestBodyBufferMiddleware(1); - $response = \React\Async\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()); @@ -214,9 +243,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/', @@ -224,18 +254,101 @@ public function testBufferingErrorThrows() new HttpBodyStream($stream, null) ); - $buffer = new RequestBodyBufferMiddleware(1); + $buffer = new RequestBodyBufferMiddleware(100); $promise = $buffer( $serverRequest, function (ServerRequestInterface $request) { - return $request; + throw new \RuntimeException('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'); - \React\Async\await($promise); + 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) { + throw new \Error('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 \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() @@ -263,4 +376,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()); + } } From 1fbe922458ccb9233bb78191a8a160bf510799b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 11 Jan 2023 15:35:07 +0100 Subject: [PATCH 115/152] Refactor to move response body handling to `ClientRequestStream` --- src/Io/ClientRequestStream.php | 22 +++++++++++++++++++++- src/Io/Sender.php | 15 ++------------- tests/Io/ClientRequestStreamTest.php | 3 ++- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index 29536e88..04671354 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -4,6 +4,8 @@ use Evenement\EventEmitter; use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use React\Http\Message\Response; use React\Promise; use React\Socket\ConnectionInterface; use React\Socket\ConnectorInterface; @@ -172,8 +174,26 @@ public function handleData($data) $this->stream->on('close', array($this, 'handleClose')); - $this->emit('response', array($response, $this->stream)); + assert($response instanceof ResponseInterface); + assert($this->stream instanceof ConnectionInterface); + $body = $this->stream; + + // 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)); + + // 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 $this->stream->emit('data', array($bodyChunk)); } } diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 2e821f5a..0894c574 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; @@ -116,18 +115,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/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index 07a4eb73..06c650ef 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -35,11 +35,12 @@ public function requestShouldBindToStreamEventsAndUseconnector() $this->successfulConnectionMock(); - $this->stream->expects($this->exactly(6))->method('on')->withConsecutive( + $this->stream->expects($this->atLeast(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'))), array('close', $this->identicalTo(array($request, 'handleClose'))) ); From 165e5b5e2c1cf9704dd2b89defdb58e49db5beaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 16 Jan 2023 14:20:11 +0100 Subject: [PATCH 116/152] Consistently close underlying connection when response stream closes --- src/Io/ClientRequestStream.php | 84 +++-- tests/Io/ClientRequestStreamTest.php | 523 ++++++++++++++++++++------- tests/TestCase.php | 6 +- 3 files changed, 448 insertions(+), 165 deletions(-) diff --git a/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index 04671354..bdaa54f1 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -16,7 +16,7 @@ * @event response * @event drain * @event error - * @event end + * @event close * @internal */ class ClientRequestStream extends EventEmitter implements WritableStreamInterface @@ -33,9 +33,11 @@ class ClientRequestStream extends EventEmitter implements WritableStreamInterfac private $request; /** @var ?ConnectionInterface */ - private $stream; + private $connection; + + /** @var string */ + private $buffer = ''; - private $buffer; private $responseFactory; private $state = self::STATE_INIT; private $ended = false; @@ -58,22 +60,22 @@ private function writeHead() $this->state = self::STATE_WRITING_HEAD; $request = $this->request; - $streamRef = &$this->stream; + $connectionRef = &$this->connection; $stateRef = &$this->state; $pendingWrites = &$this->pendingWrites; $that = $this; $promise = $this->connect(); $promise->then( - function (ConnectionInterface $stream) use ($request, &$streamRef, &$stateRef, &$pendingWrites, $that) { - $streamRef = $stream; - assert($streamRef instanceof ConnectionInterface); + function (ConnectionInterface $connection) use ($request, &$connectionRef, &$stateRef, &$pendingWrites, $that) { + $connectionRef = $connection; + assert($connectionRef instanceof ConnectionInterface); - $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')); + $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')); assert($request instanceof RequestInterface); $headers = "{$request->getMethod()} {$request->getRequestTarget()} HTTP/{$request->getProtocolVersion()}\r\n"; @@ -83,7 +85,7 @@ function (ConnectionInterface $stream) use ($request, &$streamRef, &$stateRef, & } } - $more = $stream->write($headers . "\r\n" . $pendingWrites); + $more = $connection->write($headers . "\r\n" . $pendingWrites); assert($stateRef === ClientRequestStream::STATE_WRITING_HEAD); $stateRef = ClientRequestStream::STATE_HEAD_WRITTEN; @@ -113,7 +115,7 @@ public function write($data) // write directly to connection stream if already available if (self::STATE_HEAD_WRITTEN <= $this->state) { - return $this->stream->write($data); + return $this->connection->write($data); } // otherwise buffer and try to establish connection @@ -157,26 +159,28 @@ public function handleData($data) $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)) { + $this->closeError($exception); return; } - $this->stream->on('close', array($this, 'handleClose')); - - assert($response instanceof ResponseInterface); - assert($this->stream instanceof ConnectionInterface); - $body = $this->stream; + // 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 close connection once response body closes + $that = $this; + $input = $body = new CloseProtectionStream($connection); + $input->on('close', function () use ($connection, $that) { + $connection->close(); + $that->close(); + }); // determine length of response body $length = null; @@ -194,7 +198,11 @@ public function handleData($data) $this->emit('response', array($response, $body)); // re-emit HTTP response body to trigger body parsing if parts of it are buffered - $this->stream->emit('data', array($bodyChunk)); + if ($bodyChunk !== '') { + $input->handleData($bodyChunk); + } elseif ($length === 0) { + $input->handleEnd(); + } } } @@ -216,12 +224,6 @@ public function handleError(\Exception $error) )); } - /** @internal */ - public function handleClose() - { - $this->close(); - } - /** @internal */ public function closeError(\Exception $error) { @@ -240,9 +242,11 @@ public function close() $this->state = self::STATE_END; $this->pendingWrites = ''; + $this->buffer = ''; - if ($this->stream) { - $this->stream->close(); + if ($this->connection instanceof ConnectionInterface) { + $this->connection->close(); + $this->connection = null; } $this->emit('close'); diff --git a/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index 06c650ef..93220d10 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -2,27 +2,24 @@ namespace React\Tests\Http\Io; +use Psr\Http\Message\ResponseInterface; use React\Http\Io\ClientRequestStream; use React\Http\Message\Request; use React\Promise\Deferred; use React\Promise\Promise; use React\Stream\DuplexResourceStream; +use React\Stream\ReadableStreamInterface; use React\Tests\Http\TestCase; class ClientRequestStreamTest extends TestCase { private $connector; - private $stream; /** * @before */ public function setUpStream() { - $this->stream = $this->getMockBuilder('React\Socket\ConnectionInterface') - ->disableOriginalConstructor() - ->getMock(); - $this->connector = $this->getMockBuilder('React\Socket\ConnectorInterface') ->getMock(); } @@ -30,30 +27,29 @@ public function setUpStream() /** @test */ public function requestShouldBindToStreamEventsAndUseconnector() { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $requestData = new Request('GET', 'http://www.example.com/'); $request = new ClientRequestStream($this->connector, $requestData); - $this->successfulConnectionMock(); - - $this->stream->expects($this->atLeast(6))->method('on')->withConsecutive( + $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, 'handleClose'))), - array('close', $this->identicalTo(array($request, 'handleClose'))) + array('close', $this->identicalTo(array($request, 'close'))) ); - $this->stream->expects($this->exactly(5))->method('removeListener')->withConsecutive( + $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, 'handleClose'))) + array('close', $this->identicalTo(array($request, 'close'))) ); - $request->on('end', $this->expectCallableNever()); - $request->end(); $request->handleData("HTTP/1.0 200 OK\r\n"); @@ -66,26 +62,24 @@ public function requestShouldBindToStreamEventsAndUseconnector() */ public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme() { + $this->connector->expects($this->once())->method('connect')->with('tls://www.example.com:443')->willReturn(new Promise(function () { })); + $requestData = new Request('GET', 'https://www.example.com/'); $request = new ClientRequestStream($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() { + $this->connector->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException())); + $requestData = new Request('GET', 'http://www.example.com/'); $request = new ClientRequestStream($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(); } @@ -93,15 +87,15 @@ public function requestShouldEmitErrorIfConnectionFails() /** @test */ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $requestData = new Request('GET', 'http://www.example.com/'); $request = new ClientRequestStream($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(); @@ -110,15 +104,15 @@ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() /** @test */ public function requestShouldEmitErrorIfConnectionEmitsError() { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $requestData = new Request('GET', 'http://www.example.com/'); $request = new ClientRequestStream($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')); @@ -127,12 +121,15 @@ public function requestShouldEmitErrorIfConnectionEmitsError() /** @test */ public function requestShouldEmitErrorIfRequestParserThrowsException() { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $requestData = new Request('GET', 'http://www.example.com/'); $request = new ClientRequestStream($this->connector, $requestData); - $this->successfulConnectionMock(); - $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); + $request->on('close', $this->expectCallableOnce()); $request->end(); $request->handleData("\r\n\r\n"); @@ -143,13 +140,13 @@ public function requestShouldEmitErrorIfRequestParserThrowsException() */ public function requestShouldEmitErrorIfUrlIsInvalid() { + $this->connector->expects($this->never())->method('connect'); + $requestData = new Request('GET', 'ftp://www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); - - $this->connector->expects($this->never()) - ->method('connect'); + $request->on('close', $this->expectCallableOnce()); $request->end(); } @@ -159,13 +156,13 @@ public function requestShouldEmitErrorIfUrlIsInvalid() */ public function requestShouldEmitErrorIfUrlHasNoScheme() { + $this->connector->expects($this->never())->method('connect'); + $requestData = new Request('GET', 'www.example.com'); $request = new ClientRequestStream($this->connector, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); - - $this->connector->expects($this->never()) - ->method('connect'); + $request->on('close', $this->expectCallableOnce()); $request->end(); } @@ -173,12 +170,13 @@ public function requestShouldEmitErrorIfUrlHasNoScheme() /** @test */ public function getRequestShouldSendAGetRequest() { - $requestData = new Request('GET', 'http://www.example.com/', array(), '', '1.0'); - $request = new ClientRequestStream($this->connector, $requestData); + $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"); - $this->successfulConnectionMock(); + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); - $this->stream->expects($this->once())->method('write')->with("GET / HTTP/1.0\r\nHost: www.example.com\r\n\r\n"); + $requestData = new Request('GET', 'http://www.example.com/', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); $request->end(); } @@ -186,12 +184,13 @@ public function getRequestShouldSendAGetRequest() /** @test */ public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionCloseHeader() { - $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); - $request = new ClientRequestStream($this->connector, $requestData); + $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"); - $this->successfulConnectionMock(); + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); - $this->stream->expects($this->once())->method('write')->with("GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $requestData); $request->end(); } @@ -199,29 +198,331 @@ public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionCloseHea /** @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"); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $requestData = new Request('OPTIONS', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); $requestData = $requestData->withRequestTarget('*'); $request = new ClientRequestStream($this->connector, $requestData); - $this->successfulConnectionMock(); + $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'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $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'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $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'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $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'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('HEAD', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $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'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $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'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $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'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $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'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $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'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $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'); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $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'); - $this->stream->expects($this->once())->method('write')->with("OPTIONS * HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"); + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $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; + })); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $request = new ClientRequestStream($this->connector, $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+) } /** @test */ public function postRequestShouldSendAPostRequest() { - $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0'); - $request = new ClientRequestStream($this->connector, $requestData); + $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$#")); - $this->successfulConnectionMock(); + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); - $this->stream - ->expects($this->once()) - ->method('write') - ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome post data$#")); + $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); $request->end('some post data'); @@ -233,17 +534,18 @@ public function postRequestShouldSendAPostRequest() /** @test */ public function writeWithAPostRequestShouldSendToTheStream() { - $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0'); - $request = new ClientRequestStream($this->connector, $requestData); - - $this->successfulConnectionMock(); - - $this->stream->expects($this->exactly(3))->method('write')->withConsecutive( + $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")) ); + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); + $request->write("some"); $request->write("post"); $request->end("data"); @@ -256,18 +558,20 @@ public function writeWithAPostRequestShouldSendToTheStream() /** @test */ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent() { - $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0'); - $request = new ClientRequestStream($this->connector, $requestData); - - $resolveConnection = $this->successfulAsyncConnectionMock(); - - $this->stream->expects($this->exactly(2))->method('write')->withConsecutive( + $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(); + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn($deferred->promise()); + + $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); + $this->assertFalse($request->write("some")); $this->assertFalse($request->write("post")); @@ -277,7 +581,7 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent $request->end(); }); - $resolveConnection(); + $deferred->resolve($connection); $request->handleData("HTTP/1.0 200 OK\r\n"); $request->handleData("Content-Type: text/plain\r\n"); @@ -287,23 +591,24 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent /** @test */ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsBuffer() { - $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0'); - $request = new ClientRequestStream($this->connector, $requestData); - - $this->stream = $this->getMockBuilder('React\Socket\Connection') + $connection = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() ->setMethods(array('write')) ->getMock(); - $resolveConnection = $this->successfulAsyncConnectionMock(); - - $this->stream->expects($this->exactly(2))->method('write')->withConsecutive( + $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(); + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn($deferred->promise()); + + $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); + $this->assertFalse($request->write("some")); $this->assertFalse($request->write("post")); @@ -313,8 +618,8 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB $request->end(); }); - $resolveConnection(); - $this->stream->emit('drain'); + $deferred->resolve($connection); + $connection->emit('drain'); $request->handleData("HTTP/1.0 200 OK\r\n"); $request->handleData("Content-Type: text/plain\r\n"); @@ -324,17 +629,18 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB /** @test */ public function pipeShouldPipeDataIntoTheRequestBody() { - $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0'); - $request = new ClientRequestStream($this->connector, $requestData); - - $this->successfulConnectionMock(); - - $this->stream->expects($this->exactly(3))->method('write')->withConsecutive( + $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")) ); + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + + $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0'); + $request = new ClientRequestStream($this->connector, $requestData); + $loop = $this ->getMockBuilder('React\EventLoop\LoopInterface') ->getMock(); @@ -357,14 +663,14 @@ public function pipeShouldPipeDataIntoTheRequestBody() */ public function writeShouldStartConnecting() { - $requestData = new Request('POST', 'http://www.example.com/'); - $request = new ClientRequestStream($this->connector, $requestData); - $this->connector->expects($this->once()) ->method('connect') ->with('www.example.com:80') ->willReturn(new Promise(function () { })); + $requestData = new Request('POST', 'http://www.example.com/'); + $request = new ClientRequestStream($this->connector, $requestData); + $request->write('test'); } @@ -373,14 +679,11 @@ public function writeShouldStartConnecting() */ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() { + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(new Promise(function () { })); + $requestData = new Request('POST', 'http://www.example.com/'); $request = new ClientRequestStream($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()); @@ -417,12 +720,11 @@ public function writeAfterCloseReturnsFalse() */ public function endAfterCloseIsNoOp() { + $this->connector->expects($this->never())->method('connect'); + $requestData = new Request('POST', 'http://www.example.com/'); $request = new ClientRequestStream($this->connector, $requestData); - $this->connector->expects($this->never()) - ->method('connect'); - $request->close(); $request->end(); } @@ -432,17 +734,13 @@ public function endAfterCloseIsNoOp() */ public function closeShouldCancelPendingConnectionAttempt() { - $requestData = new Request('POST', 'http://www.example.com/'); - $request = new ClientRequestStream($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); - $this->connector->expects($this->once()) - ->method('connect') - ->with('www.example.com:80') - ->willReturn($promise); + $requestData = new Request('POST', 'http://www.example.com/'); + $request = new ClientRequestStream($this->connector, $requestData); $request->end(); @@ -466,35 +764,16 @@ public function requestShouldRemoveAllListenerAfterClosed() $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() { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $requestData = new Request('GET', 'http://www.example.com/'); $request = new ClientRequestStream($this->connector, $requestData); - $this->successfulConnectionMock(); - $response = null; $request->on('response', $this->expectCallableOnce()); $request->on('response', function ($value) use (&$response) { 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 From 28b598ab09109da412e1935d9560888887cfb86a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 12 Jan 2023 10:58:56 +0100 Subject: [PATCH 117/152] Send `Connection: close` for HTTP/1.1 and no `Connection` for HTTP/1.0 --- src/Io/Sender.php | 4 +++- tests/Io/SenderTest.php | 20 +++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 0894c574..acbb6e7d 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -94,8 +94,10 @@ public function send(RequestInterface $request) } // automatically add `Connection: close` request header for HTTP/1.1 requests to avoid connection reuse - if ($request->getProtocolVersion() === '1.1' && !$request->hasHeader('Connection')) { + if ($request->getProtocolVersion() === '1.1') { $request = $request->withHeader('Connection', 'close'); + } else { + $request = $request->withoutHeader('Connection'); } // automatically add `Authorization: Basic …` request header if URL includes `user:pass@host` diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index c2357a1a..4ef06442 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -302,6 +302,20 @@ public function getHttp10RequestShouldSendAGetRequestWithoutConnectionHeaderByDe $sender->send($request); } + /** @test */ + public function getHttp10RequestShouldSendAGetRequestWithoutConnectionHeaderEvenWhenConnectionKeepAliveHeaderIsSpecified() + { + $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { + return !$request->hasHeader('Connection'); + }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); + + $sender = new Sender($client); + + $request = new Request('GET', 'http://www.example.com/', array('Connection' => 'keep-alive'), '', '1.0'); + $sender->send($request); + } + /** @test */ public function getHttp11RequestShouldSendAGetRequestWithConnectionCloseHeaderByDefault() { @@ -317,16 +331,16 @@ public function getHttp11RequestShouldSendAGetRequestWithConnectionCloseHeaderBy } /** @test */ - public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionUpgradeHeader() + public function getHttp11RequestShouldSendAGetRequestWithConnectionCloseHeaderEvenWhenConnectionKeepAliveHeaderIsSpecified() { $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { - return $request->getHeaderLine('Connection') === 'upgrade'; + return $request->getHeaderLine('Connection') === 'close'; }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); $sender = new Sender($client); - $request = new Request('GET', 'http://www.example.com/', array('Connection' => 'upgrade'), '', '1.1'); + $request = new Request('GET', 'http://www.example.com/', array('Connection' => 'keep-alive'), '', '1.1'); $sender->send($request); } From d8566953c6b699618a500a4589833c19d6985f2b Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 21 Jan 2023 14:52:26 +0100 Subject: [PATCH 118/152] Template params can only have one argument The fact that a promise can also be rejected with a Throwable and/or Exception is implied and there is no need to also define that here. Refs: https://github.com/reactphp/promise/pull/223 --- src/Browser.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Browser.php b/src/Browser.php index 3e3458af..b7bf4425 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -344,7 +344,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 = '') { @@ -417,7 +417,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 = '') { @@ -828,7 +828,7 @@ 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 = '') { From 1c911d2fff297278d74815008dfe95b0036379b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 10 Jan 2023 15:49:44 +0100 Subject: [PATCH 119/152] Refactor to add new `ClientConnectionManager` to manage HTTP connections --- src/Client/Client.php | 17 +- src/Io/ClientConnectionManager.php | 45 ++++ src/Io/ClientRequestStream.php | 37 +--- src/Io/Sender.php | 7 +- tests/BrowserTest.php | 16 +- tests/Client/FunctionalIntegrationTest.php | 15 +- tests/Io/ClientConnectionManagerTest.php | 83 ++++++++ tests/Io/ClientRequestStreamTest.php | 227 ++++++++++----------- tests/Io/SenderTest.php | 23 +-- 9 files changed, 276 insertions(+), 194 deletions(-) create mode 100644 src/Io/ClientConnectionManager.php create mode 100644 tests/Io/ClientConnectionManagerTest.php diff --git a/src/Client/Client.php b/src/Client/Client.php index c3fd4570..7a5180ab 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -3,30 +3,25 @@ namespace React\Http\Client; use Psr\Http\Message\RequestInterface; -use React\EventLoop\LoopInterface; +use React\Http\Io\ClientConnectionManager; use React\Http\Io\ClientRequestStream; -use React\Socket\Connector; -use React\Socket\ConnectorInterface; /** * @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; } /** @return ClientRequestStream */ public function request(RequestInterface $request) { - return new ClientRequestStream($this->connector, $request); + return new ClientRequestStream($this->connectionManager, $request); } } diff --git a/src/Io/ClientConnectionManager.php b/src/Io/ClientConnectionManager.php new file mode 100644 index 00000000..51f937e4 --- /dev/null +++ b/src/Io/ClientConnectionManager.php @@ -0,0 +1,45 @@ +connector = $connector; + } + + /** + * @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; + } + + return $this->connector->connect(($scheme === 'https' ? 'tls://' : '') . $uri->getHost() . ':' . $port); + } +} diff --git a/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index bdaa54f1..e5eaf298 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -4,11 +4,8 @@ use Evenement\EventEmitter; use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; use React\Http\Message\Response; -use React\Promise; use React\Socket\ConnectionInterface; -use React\Socket\ConnectorInterface; use React\Stream\WritableStreamInterface; use RingCentral\Psr7 as gPsr; @@ -26,8 +23,8 @@ class ClientRequestStream extends EventEmitter implements WritableStreamInterfac const STATE_HEAD_WRITTEN = 2; const STATE_END = 3; - /** @var ConnectorInterface */ - private $connector; + /** @var ClientConnectionManager */ + private $connectionManager; /** @var RequestInterface */ private $request; @@ -44,9 +41,9 @@ class ClientRequestStream extends EventEmitter implements WritableStreamInterfac private $pendingWrites = ''; - public function __construct(ConnectorInterface $connector, RequestInterface $request) + public function __construct(ClientConnectionManager $connectionManager, RequestInterface $request) { - $this->connector = $connector; + $this->connectionManager = $connectionManager; $this->request = $request; } @@ -65,7 +62,7 @@ private function writeHead() $pendingWrites = &$this->pendingWrites; $that = $this; - $promise = $this->connect(); + $promise = $this->connectionManager->connect($this->request->getUri()); $promise->then( function (ConnectionInterface $connection) use ($request, &$connectionRef, &$stateRef, &$pendingWrites, $that) { $connectionRef = $connection; @@ -252,28 +249,4 @@ public function close() $this->emit('close'); $this->removeAllListeners(); } - - protected function connect() - { - $scheme = $this->request->getUri()->getScheme(); - if ($scheme !== 'https' && $scheme !== 'http') { - return Promise\reject( - new \InvalidArgumentException('Invalid request URL given') - ); - } - - $host = $this->request->getUri()->getHost(); - $port = $this->request->getUri()->getPort(); - - if ($scheme === 'https') { - $host = 'tls://' . $host; - } - - if ($port === null) { - $port = $scheme === 'https' ? 443 : 80; - } - - return $this->connector - ->connect($host . ':' . $port); - } } diff --git a/src/Io/Sender.php b/src/Io/Sender.php index acbb6e7d..68c09322 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -8,6 +8,7 @@ use React\Http\Client\Client as HttpClient; use React\Promise\PromiseInterface; use React\Promise\Deferred; +use React\Socket\Connector; use React\Socket\ConnectorInterface; use React\Stream\ReadableStreamInterface; @@ -49,7 +50,11 @@ class Sender */ public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector = null) { - return new self(new HttpClient($loop, $connector)); + if ($connector === null) { + $connector = new Connector(array(), $loop); + } + + return new self(new HttpClient(new ClientConnectionManager($connector))); } private $http; diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index ad61cf9b..21242a5d 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -60,9 +60,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); } @@ -85,9 +89,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); } diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 90d8444b..d5015fd1 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -3,12 +3,13 @@ namespace React\Tests\Http\Client; 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\Stream; use React\Socket\ConnectionInterface; +use React\Socket\Connector; use React\Socket\SocketServer; use React\Stream\ReadableStreamInterface; use React\Tests\Http\TestCase; @@ -45,7 +46,7 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() }); $port = parse_url(https://github.com/$socket-%3EgetAddress(), PHP_URL_PORT); - $client = new Client(Loop::get()); + $client = new Client(new ClientConnectionManager(new Connector())); $request = $client->request(new Request('GET', 'http://localhost/' . $port, array(), '', '1.0')); $promise = Stream\first($request, 'close'); @@ -62,7 +63,7 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp $socket->close(); }); - $client = new Client(Loop::get()); + $client = new Client(new ClientConnectionManager(new Connector())); $request = $client->request(new Request('GET', str_replace('tcp:', 'http:', $socket->getAddress()), array(), '', '1.0')); $once = $this->expectCallableOnceWith('body'); @@ -82,7 +83,7 @@ public function testSuccessfulResponseEmitsEnd() // 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())); $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); @@ -107,7 +108,7 @@ public function testPostDataReturnsData() // 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())); $data = str_repeat('.', 33000); $request = $client->request(new Request('POST', 'https://' . (mt_rand(0, 1) === 0 ? 'eu.' : '') . 'httpbin.org/post', array('Content-Length' => strlen($data)), '', '1.0')); @@ -139,7 +140,7 @@ public function testPostJsonReturnsData() $this->markTestSkipped('Not supported on HHVM'); } - $client = new Client(Loop::get()); + $client = new Client(new ClientConnectionManager(new Connector())); $data = json_encode(array('numbers' => range(1, 50))); $request = $client->request(new Request('POST', 'https://httpbin.org/post', array('Content-Length' => strlen($data), 'Content-Type' => 'application/json'), '', '1.0')); @@ -169,7 +170,7 @@ 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())); $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); $request->on('error', $this->expectCallableNever()); diff --git a/tests/Io/ClientConnectionManagerTest.php b/tests/Io/ClientConnectionManagerTest.php new file mode 100644 index 00000000..5774e47d --- /dev/null +++ b/tests/Io/ClientConnectionManagerTest.php @@ -0,0 +1,83 @@ +getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn($promise); + + $connectionManager = new ClientConnectionManager($connector); + + $ret = $connectionManager->connect(new Uri('https://reactphp.org/')); + $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); + + $connectionManager = new ClientConnectionManager($connector); + + $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); + + $connectionManager = new ClientConnectionManager($connector); + + $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'); + + $connectionManager = new ClientConnectionManager($connector); + + $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'); + + $connectionManager = new ClientConnectionManager($connector); + + $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()); + } +} diff --git a/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index 93220d10..da26b4e5 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -3,6 +3,7 @@ namespace React\Tests\Http\Io; use Psr\Http\Message\ResponseInterface; +use RingCentral\Psr7\Uri; use React\Http\Io\ClientRequestStream; use React\Http\Message\Request; use React\Promise\Deferred; @@ -13,26 +14,17 @@ class ClientRequestStreamTest extends TestCase { - private $connector; - - /** - * @before - */ - public function setUpStream() - { - $this->connector = $this->getMockBuilder('React\Socket\ConnectorInterface') - ->getMock(); - } - /** @test */ - public function requestShouldBindToStreamEventsAndUseconnector() + public function testRequestShouldUseConnectionManagerWithUriFromRequestAndBindToStreamEvents() { $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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', 'http://www.example.com/'); - $request = new ClientRequestStream($this->connector, $requestData); + $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'))), @@ -57,26 +49,14 @@ public function requestShouldBindToStreamEventsAndUseconnector() $request->handleData("\r\nbody"); } - /** - * @test - */ - public function requestShouldConnectViaTlsIfUrlUsesHttpsScheme() - { - $this->connector->expects($this->once())->method('connect')->with('tls://www.example.com:443')->willReturn(new Promise(function () { })); - - $requestData = new Request('GET', 'https://www.example.com/'); - $request = new ClientRequestStream($this->connector, $requestData); - - $request->end(); - } - /** @test */ public function requestShouldEmitErrorIfConnectionFails() { - $this->connector->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException())); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); $request->on('close', $this->expectCallableOnce()); @@ -89,10 +69,11 @@ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() { $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); $request->on('close', $this->expectCallableOnce()); @@ -106,10 +87,11 @@ public function requestShouldEmitErrorIfConnectionEmitsError() { $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('Exception'))); $request->on('close', $this->expectCallableOnce()); @@ -123,10 +105,11 @@ public function requestShouldEmitErrorIfRequestParserThrowsException() { $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); $request->on('close', $this->expectCallableOnce()); @@ -135,48 +118,17 @@ public function requestShouldEmitErrorIfRequestParserThrowsException() $request->handleData("\r\n\r\n"); } - /** - * @test - */ - public function requestShouldEmitErrorIfUrlIsInvalid() - { - $this->connector->expects($this->never())->method('connect'); - - $requestData = new Request('GET', 'ftp://www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); - - $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); - $request->on('close', $this->expectCallableOnce()); - - $request->end(); - } - - /** - * @test - */ - public function requestShouldEmitErrorIfUrlHasNoScheme() - { - $this->connector->expects($this->never())->method('connect'); - - $requestData = new Request('GET', 'www.example.com'); - $request = new ClientRequestStream($this->connector, $requestData); - - $request->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); - $request->on('close', $this->expectCallableOnce()); - - $request->end(); - } - /** @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"); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->end(); } @@ -187,10 +139,11 @@ public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionCloseHea $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"); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->end(); } @@ -201,11 +154,12 @@ 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"); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->end(); } @@ -216,10 +170,11 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsCon $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'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -240,10 +195,11 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsSta $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'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -264,10 +220,11 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsSta $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'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -288,10 +245,11 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenRequestMethodIsHead $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'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -312,10 +270,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons $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'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -336,10 +295,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResp $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'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -360,10 +320,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndW $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'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -384,10 +345,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons $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'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -408,10 +370,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResp $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'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -432,10 +395,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndW $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'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -456,10 +420,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndW $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'); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -492,10 +457,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons return true; })); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $that = $this; $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($that) { @@ -519,10 +485,11 @@ 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$#")); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->end('some post data'); @@ -541,10 +508,11 @@ public function writeWithAPostRequestShouldSendToTheStream() array($this->identicalTo("data")) ); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->write("some"); $request->write("post"); @@ -567,10 +535,11 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent ); $deferred = new Deferred(); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn($deferred->promise()); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $this->assertFalse($request->write("some")); $this->assertFalse($request->write("post")); @@ -604,10 +573,11 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB ); $deferred = new Deferred(); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn($deferred->promise()); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $this->assertFalse($request->write("some")); $this->assertFalse($request->write("post")); @@ -636,10 +606,11 @@ public function pipeShouldPipeDataIntoTheRequestBody() array($this->identicalTo("data")) ); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $loop = $this ->getMockBuilder('React\EventLoop\LoopInterface') @@ -663,13 +634,11 @@ public function pipeShouldPipeDataIntoTheRequestBody() */ public function writeShouldStartConnecting() { - $this->connector->expects($this->once()) - ->method('connect') - ->with('www.example.com:80') - ->willReturn(new Promise(function () { })); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->write('test'); } @@ -679,10 +648,11 @@ public function writeShouldStartConnecting() */ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() { - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(new Promise(function () { })); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->end(); @@ -694,8 +664,10 @@ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() */ public function closeShouldEmitCloseEvent() { + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $requestData = new Request('POST', 'http://www.example.com/'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); $request->close(); @@ -706,8 +678,10 @@ public function closeShouldEmitCloseEvent() */ public function writeAfterCloseReturnsFalse() { + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $requestData = new Request('POST', 'http://www.example.com/'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->close(); @@ -720,10 +694,11 @@ public function writeAfterCloseReturnsFalse() */ public function endAfterCloseIsNoOp() { - $this->connector->expects($this->never())->method('connect'); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->close(); $request->end(); @@ -737,10 +712,11 @@ public function closeShouldCancelPendingConnectionAttempt() $promise = new Promise(function () {}, function () { throw new \RuntimeException(); }); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn($promise); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->end(); @@ -754,8 +730,10 @@ public function closeShouldCancelPendingConnectionAttempt() /** @test */ public function requestShouldRemoveAllListenerAfterClosed() { + $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $requestData = new Request('GET', 'http://www.example.com/'); - $request = new ClientRequestStream($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', function () {}); $this->assertCount(1, $request->listeners('close')); @@ -769,10 +747,11 @@ public function multivalueHeader() { $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); - $this->connector->expects($this->once())->method('connect')->with('www.example.com:80')->willReturn(\React\Promise\resolve($connection)); + $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($this->connector, $requestData); + $request = new ClientRequestStream($connectionManager, $requestData); $response = null; $request->on('response', $this->expectCallableOnce()); diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 4ef06442..220424e9 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -4,6 +4,7 @@ use Psr\Http\Message\RequestInterface; use React\Http\Client\Client as HttpClient; +use React\Http\Io\ClientConnectionManager; use React\Http\Io\ReadableBodyStream; use React\Http\Io\Sender; use React\Http\Message\Request; @@ -13,19 +14,11 @@ class SenderTest extends TestCase { - private $loop; - - /** - * @before - */ - public function setUpLoop() - { - $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - } - public function testCreateFromLoop() { - $sender = Sender::createFromLoop($this->loop, null); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $sender = Sender::createFromLoop($loop, null); $this->assertInstanceOf('React\Http\Io\Sender', $sender); } @@ -35,7 +28,7 @@ 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))); $request = new Request('GET', 'www.google.com'); @@ -54,7 +47,7 @@ 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))); $request = new Request('GET', 'http://www.google.com/'); @@ -381,7 +374,7 @@ 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))); $request = new Request('GET', 'http://www.google.com/'); @@ -404,7 +397,7 @@ 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))); $request = new Request('GET', 'http://www.google.com/'); From ab3bfee58c16cfb51691c136f0ec2f5e53268e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 17 Jan 2023 10:24:28 +0100 Subject: [PATCH 120/152] Prepare to hand back connections when keep-alive is possible --- src/Io/ClientConnectionManager.php | 8 + src/Io/ClientRequestStream.php | 44 +++- tests/Client/FunctionalIntegrationTest.php | 25 +++ tests/Io/ClientConnectionManagerTest.php | 12 ++ tests/Io/ClientRequestStreamTest.php | 234 +++++++++++++++++++++ 5 files changed, 320 insertions(+), 3 deletions(-) diff --git a/src/Io/ClientConnectionManager.php b/src/Io/ClientConnectionManager.php index 51f937e4..eda2ea44 100644 --- a/src/Io/ClientConnectionManager.php +++ b/src/Io/ClientConnectionManager.php @@ -42,4 +42,12 @@ public function connect(UriInterface $uri) return $this->connector->connect(($scheme === 'https' ? 'tls://' : '') . $uri->getHost() . ':' . $port); } + + /** + * @return void + */ + public function handBack(ConnectionInterface $connection) + { + $connection->close(); + } } diff --git a/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index e5eaf298..e9716b45 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -3,6 +3,7 @@ namespace React\Http\Io; use Evenement\EventEmitter; +use Psr\Http\Message\MessageInterface; use Psr\Http\Message\RequestInterface; use React\Http\Message\Response; use React\Socket\ConnectionInterface; @@ -171,11 +172,20 @@ public function handleData($data) $this->connection = null; $this->buffer = ''; - // take control over connection handling and close connection once response body closes + // 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) { - $connection->close(); + $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->handBack($connection); + } else { + $connection->close(); + } + $that->close(); }); @@ -190,6 +200,9 @@ public function handleData($data) $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)); @@ -249,4 +262,29 @@ public function close() $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) + { + $connectionOptions = \RingCentral\Psr7\normalize_header(\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/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index d5015fd1..1c37d897 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -7,6 +7,7 @@ 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; @@ -55,6 +56,30 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); } + public function testRequestToLocalhostWillConnectAndCloseConnectionAfterResponseUntilKeepAliveIsActuallySupported() + { + $socket = new SocketServer('127.0.0.1:0'); + $socket->on('connection', $this->expectCallableOnce()); + + $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())); + $request = $client->request(new Request('GET', 'http://localhost/' . $port, array(), '', '1.1')); + + $request->end(); + + \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); + } + public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResponse() { $socket = new SocketServer('127.0.0.1:0'); diff --git a/tests/Io/ClientConnectionManagerTest.php b/tests/Io/ClientConnectionManagerTest.php index 5774e47d..143676e4 100644 --- a/tests/Io/ClientConnectionManagerTest.php +++ b/tests/Io/ClientConnectionManagerTest.php @@ -80,4 +80,16 @@ public function testConnectWithoutSchemeShouldRejectWithException() $this->assertInstanceOf('InvalidArgumentException', $exception); $this->assertEquals('Invalid request URL given', $exception->getMessage()); } + + public function testHandBackWillCloseGivenConnectionUntilKeepAliveIsActuallySupported() + { + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + + $connectionManager = new ClientConnectionManager($connector); + + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('close'); + + $connectionManager->handBack($connection); + } } diff --git a/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index da26b4e5..d257994b 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -479,6 +479,240 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons 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('handBack')->with($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('handBack')->with($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('handBack')->with($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('handBack')->with($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() { From 28943f443a54ba4c2576ce68ee76274d190b6162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 11 Jan 2023 13:28:15 +0100 Subject: [PATCH 121/152] Reuse existing connections for HTTP keep-alive --- src/Io/ClientConnectionManager.php | 90 +++++- src/Io/ClientRequestStream.php | 2 +- src/Io/Sender.php | 2 +- tests/Client/FunctionalIntegrationTest.php | 45 ++- tests/Io/ClientConnectionManagerTest.php | 310 ++++++++++++++++++++- tests/Io/ClientRequestStreamTest.php | 8 +- tests/Io/SenderTest.php | 23 +- 7 files changed, 448 insertions(+), 32 deletions(-) diff --git a/src/Io/ClientConnectionManager.php b/src/Io/ClientConnectionManager.php index eda2ea44..faac98b6 100644 --- a/src/Io/ClientConnectionManager.php +++ b/src/Io/ClientConnectionManager.php @@ -3,6 +3,8 @@ namespace React\Http\Io; use Psr\Http\Message\UriInterface; +use React\EventLoop\LoopInterface; +use React\EventLoop\TimerInterface; use React\Promise\PromiseInterface; use React\Socket\ConnectionInterface; use React\Socket\ConnectorInterface; @@ -18,9 +20,28 @@ class ClientConnectionManager /** @var ConnectorInterface */ private $connector; - public function __construct(ConnectorInterface $connector) + /** @var LoopInterface */ + private $loop; + + /** @var string[] */ + private $idleUris = array(); + + /** @var ConnectionInterface[] */ + private $idleConnections = array(); + + /** @var TimerInterface[] */ + private $idleTimers = array(); + + /** @var \Closure[] */ + private $idleStreamHandlers = array(); + + /** @var float */ + private $maximumTimeToKeepAliveIdleConnection = 0.001; + + public function __construct(ConnectorInterface $connector, LoopInterface $loop) { $this->connector = $connector; + $this->loop = $loop; } /** @@ -39,15 +60,78 @@ public function connect(UriInterface $uri) 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 $this->connector->connect(($scheme === 'https' ? 'tls://' : '') . $uri->getHost() . ':' . $port); + 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 handBack(ConnectionInterface $connection) + 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/ClientRequestStream.php b/src/Io/ClientRequestStream.php index e9716b45..0220f008 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -181,7 +181,7 @@ public function handleData($data) $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->handBack($connection); + $connectionManager->keepAlive($request->getUri(), $connection); } else { $connection->close(); } diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 68c09322..c117d87d 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -54,7 +54,7 @@ public static function createFromLoop(LoopInterface $loop, ConnectorInterface $c $connector = new Connector(array(), $loop); } - return new self(new HttpClient(new ClientConnectionManager($connector))); + return new self(new HttpClient(new ClientConnectionManager($connector, $loop))); } private $http; diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 1c37d897..4925239c 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -3,6 +3,7 @@ namespace React\Tests\Http\Client; use Psr\Http\Message\ResponseInterface; +use React\EventLoop\Loop; use React\Http\Client\Client; use React\Http\Io\ClientConnectionManager; use React\Http\Message\Request; @@ -47,7 +48,7 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() }); $port = parse_url(https://github.com/$socket-%3EgetAddress(), PHP_URL_PORT); - $client = new Client(new ClientConnectionManager(new Connector())); + $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'); @@ -56,7 +57,7 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); } - public function testRequestToLocalhostWillConnectAndCloseConnectionAfterResponseUntilKeepAliveIsActuallySupported() + public function testRequestToLocalhostWillConnectAndCloseConnectionAfterResponseWhenKeepAliveTimesOut() { $socket = new SocketServer('127.0.0.1:0'); $socket->on('connection', $this->expectCallableOnce()); @@ -72,7 +73,7 @@ public function testRequestToLocalhostWillConnectAndCloseConnectionAfterResponse }); $port = parse_url(https://github.com/$socket-%3EgetAddress(), PHP_URL_PORT); - $client = new Client(new ClientConnectionManager(new Connector())); + $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); $request = $client->request(new Request('GET', 'http://localhost/' . $port, array(), '', '1.1')); $request->end(); @@ -80,6 +81,34 @@ public function testRequestToLocalhostWillConnectAndCloseConnectionAfterResponse \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); } + public function testRequestToLocalhostWillReuseExistingConnectionForSecondRequest() + { + $socket = new SocketServer('127.0.0.1:0'); + $socket->on('connection', $this->expectCallableOnce()); + + $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); + + $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(); + + \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(); + + \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); + } + public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResponse() { $socket = new SocketServer('127.0.0.1:0'); @@ -88,7 +117,7 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp $socket->close(); }); - $client = new Client(new ClientConnectionManager(new Connector())); + $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); $request = $client->request(new Request('GET', str_replace('tcp:', 'http:', $socket->getAddress()), array(), '', '1.0')); $once = $this->expectCallableOnceWith('body'); @@ -108,7 +137,7 @@ public function testSuccessfulResponseEmitsEnd() // 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(new ClientConnectionManager(new Connector())); + $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); @@ -133,7 +162,7 @@ public function testPostDataReturnsData() // 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(new ClientConnectionManager(new Connector())); + $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); $data = str_repeat('.', 33000); $request = $client->request(new Request('POST', 'https://' . (mt_rand(0, 1) === 0 ? 'eu.' : '') . 'httpbin.org/post', array('Content-Length' => strlen($data)), '', '1.0')); @@ -165,7 +194,7 @@ public function testPostJsonReturnsData() $this->markTestSkipped('Not supported on HHVM'); } - $client = new Client(new ClientConnectionManager(new Connector())); + $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); $data = json_encode(array('numbers' => range(1, 50))); $request = $client->request(new Request('POST', 'https://httpbin.org/post', array('Content-Length' => strlen($data), 'Content-Type' => 'application/json'), '', '1.0')); @@ -195,7 +224,7 @@ 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(new ClientConnectionManager(new Connector())); + $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); $request->on('error', $this->expectCallableNever()); diff --git a/tests/Io/ClientConnectionManagerTest.php b/tests/Io/ClientConnectionManagerTest.php index 143676e4..b28c7964 100644 --- a/tests/Io/ClientConnectionManagerTest.php +++ b/tests/Io/ClientConnectionManagerTest.php @@ -5,6 +5,7 @@ use RingCentral\Psr7\Uri; use React\Http\Io\ClientConnectionManager; use React\Promise\Promise; +use React\Promise\PromiseInterface; use React\Tests\Http\TestCase; class ClientConnectionManagerTest extends TestCase @@ -15,9 +16,13 @@ public function testConnectWithHttpsUriShouldConnectToTlsWithDefaultPort() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn($promise); - $connectionManager = new ClientConnectionManager($connector); + $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); } @@ -27,7 +32,9 @@ public function testConnectWithHttpUriShouldConnectToTcpWithDefaultPort() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->with('reactphp.org:80')->willReturn($promise); - $connectionManager = new ClientConnectionManager($connector); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $connectionManager = new ClientConnectionManager($connector, $loop); $ret = $connectionManager->connect(new Uri('http://reactphp.org/')); $this->assertSame($promise, $ret); @@ -39,7 +46,9 @@ public function testConnectWithExplicitPortShouldConnectWithGivenPort() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->with('reactphp.org:8080')->willReturn($promise); - $connectionManager = new ClientConnectionManager($connector); + $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); @@ -50,7 +59,9 @@ public function testConnectWithInvalidSchemeShouldRejectWithException() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->never())->method('connect'); - $connectionManager = new ClientConnectionManager($connector); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $connectionManager = new ClientConnectionManager($connector, $loop); $promise = $connectionManager->connect(new Uri('ftp://reactphp.org/')); @@ -68,7 +79,9 @@ public function testConnectWithoutSchemeShouldRejectWithException() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->never())->method('connect'); - $connectionManager = new ClientConnectionManager($connector); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $connectionManager = new ClientConnectionManager($connector, $loop); $promise = $connectionManager->connect(new Uri('reactphp.org')); @@ -81,15 +94,296 @@ public function testConnectWithoutSchemeShouldRejectWithException() $this->assertEquals('Invalid request URL given', $exception->getMessage()); } - public function testHandBackWillCloseGivenConnectionUntilKeepAliveIsActuallySupported() + 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 = new ClientConnectionManager($connector); + $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'); - $connectionManager->handBack($connection); + $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 index d257994b..4649087a 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -488,7 +488,7 @@ public function testStreamShouldReuseConnectionForHttp11ByDefault() $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('handBack')->with($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); @@ -569,7 +569,7 @@ public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndRespon $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('handBack')->with($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); @@ -590,7 +590,7 @@ public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndRespon $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('handBack')->with($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); @@ -681,7 +681,7 @@ public function testStreamShouldReuseConnectionWhenResponseContainsTransferEncod $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('handBack')->with($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); diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 220424e9..0f555f9c 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -14,11 +14,20 @@ class SenderTest extends TestCase { - public function testCreateFromLoop() + /** @var \React\EventLoop\LoopInterface */ + private $loop; + + /** + * @before + */ + public function setUpLoop() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + } - $sender = Sender::createFromLoop($loop, null); + public function testCreateFromLoop() + { + $sender = Sender::createFromLoop($this->loop, null); $this->assertInstanceOf('React\Http\Io\Sender', $sender); } @@ -28,7 +37,7 @@ public function testSenderRejectsInvalidUri() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->never())->method('connect'); - $sender = new Sender(new HttpClient(new ClientConnectionManager($connector))); + $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); $request = new Request('GET', 'www.google.com'); @@ -47,7 +56,7 @@ 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(new ClientConnectionManager($connector))); + $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); $request = new Request('GET', 'http://www.google.com/'); @@ -374,7 +383,7 @@ public function testCancelRequestWillCancelConnector() $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn($promise); - $sender = new Sender(new HttpClient(new ClientConnectionManager($connector))); + $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); $request = new Request('GET', 'http://www.google.com/'); @@ -397,7 +406,7 @@ 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(new ClientConnectionManager($connector))); + $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); $request = new Request('GET', 'http://www.google.com/'); From ebaf6f132cd821230447a2287be79d3c3c23d625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 19 Jan 2023 19:16:28 +0100 Subject: [PATCH 122/152] Add `Connection: close` default header to allow toggling keep-alive --- src/Browser.php | 1 + src/Io/Sender.php | 7 ----- src/Io/Transaction.php | 2 +- tests/BrowserTest.php | 28 +++++++++++++++++ tests/FunctionalBrowserTest.php | 54 +++++++++++++++++++++++++++++++ tests/Io/SenderTest.php | 56 --------------------------------- 6 files changed, 84 insertions(+), 64 deletions(-) diff --git a/src/Browser.php b/src/Browser.php index 3e3458af..12bce6b5 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -23,6 +23,7 @@ class Browser private $baseUrl; private $protocolVersion = '1.1'; private $defaultHeaders = array( + 'Connection' => 'close', 'User-Agent' => 'ReactPHP/1' ); diff --git a/src/Io/Sender.php b/src/Io/Sender.php index c117d87d..3598d31a 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -98,13 +98,6 @@ public function send(RequestInterface $request) $size = 0; } - // automatically add `Connection: close` request header for HTTP/1.1 requests to avoid connection reuse - if ($request->getProtocolVersion() === '1.1') { - $request = $request->withHeader('Connection', 'close'); - } else { - $request = $request->withoutHeader('Connection'); - } - // 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())); diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index bfa42241..b93c490c 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -302,7 +302,7 @@ private function makeRedirectRequest(RequestInterface $request, UriInterface $lo ->withMethod($request->getMethod() === 'HEAD' ? 'HEAD' : 'GET') ->withoutHeader('Content-Type') ->withoutHeader('Content-Length') - ->withBody(new EmptyBodyStream()); + ->withBody(new BufferedBody('')); } return $request; diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index 21242a5d..d01de9c5 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -556,6 +556,8 @@ public function testWithMultipleHeadersShouldBeMergedCorrectlyWithMultipleDefaul 'user-Agent' => array('ABC'), 'another-header' => array('value'), 'custom-header' => array('data'), + + 'Connection' => array('close') ); $that->assertEquals($expectedHeaders, $request->getHeaders()); @@ -584,6 +586,32 @@ public function testWithoutHeaderShouldRemoveExistingHeader() $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; diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 6def2ecc..7ab909de 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -553,6 +553,60 @@ public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeeps $socket->close(); } + public function testRequestWillCreateNewConnectionForSecondRequestByDefaultEvenWhenServerKeepsConnectionOpen() + { + $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()) . '/'; + + $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 testRequestWithoutConnectionHeaderWillReuseExistingConnectionForSecondRequest() + { + $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 . '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(); diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 0f555f9c..3c8c4761 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -290,62 +290,6 @@ public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsI $sender->send($request); } - /** @test */ - public function getHttp10RequestShouldSendAGetRequestWithoutConnectionHeaderByDefault() - { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { - return !$request->hasHeader('Connection'); - }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); - - $sender = new Sender($client); - - $request = new Request('GET', 'http://www.example.com/', array(), '', '1.0'); - $sender->send($request); - } - - /** @test */ - public function getHttp10RequestShouldSendAGetRequestWithoutConnectionHeaderEvenWhenConnectionKeepAliveHeaderIsSpecified() - { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { - return !$request->hasHeader('Connection'); - }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); - - $sender = new Sender($client); - - $request = new Request('GET', 'http://www.example.com/', array('Connection' => 'keep-alive'), '', '1.0'); - $sender->send($request); - } - - /** @test */ - public function getHttp11RequestShouldSendAGetRequestWithConnectionCloseHeaderByDefault() - { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { - return $request->getHeaderLine('Connection') === 'close'; - }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); - - $sender = new Sender($client); - - $request = new Request('GET', 'http://www.example.com/', array(), '', '1.1'); - $sender->send($request); - } - - /** @test */ - public function getHttp11RequestShouldSendAGetRequestWithConnectionCloseHeaderEvenWhenConnectionKeepAliveHeaderIsSpecified() - { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); - $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { - return $request->getHeaderLine('Connection') === 'close'; - }))->willReturn($this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock()); - - $sender = new Sender($client); - - $request = new Request('GET', 'http://www.example.com/', array('Connection' => 'keep-alive'), '', '1.1'); - $sender->send($request); - } - /** @test */ public function getRequestWithUserAndPassShouldSendAGetRequestWithBasicAuthorizationHeader() { From b3594f7936b92f9fc2d5f9e84dc01bdb95a72167 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 16 Feb 2023 17:25:39 +0100 Subject: [PATCH 123/152] Stop parsing multipart request bodies once the configured limit of form fields and files has been reached This fix is inspired by how PHP is handling it but without following the ini setting. Such setting isn't needed as the limits on files and form fields are enough. --- src/Io/MultipartParser.php | 15 +++++++++++++++ tests/Io/MultipartParserTest.php | 27 ++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 536694fd..6a874336 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,6 +69,7 @@ final class MultipartParser */ private $maxFileUploads; + private $multipartBodyPartCount = 0; private $postCount = 0; private $filesCount = 0; private $emptyCount = 0; @@ -87,6 +95,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 +111,7 @@ public function parse(ServerRequestInterface $request) $request = $this->request; $this->request = null; + $this->multipartBodyPartCount = 0; $this->postCount = 0; $this->filesCount = 0; $this->emptyCount = 0; @@ -128,6 +139,10 @@ private function parseBody($boundary, $buffer) // parse one part and continue searching for next $this->parsePart(\substr($buffer, $start, $end - $start)); $start = $end; + + if (++$this->multipartBodyPartCount > $this->maxMultipartBodyParts) { + break; + } } } diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 14550f57..5dfd6e43 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -1026,4 +1026,29 @@ 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() + { + $boundary = "---------------------------12758086162038677464950549563"; + + $chunk = "--$boundary\r\n"; + $chunk .= "Content-Disposition: form-data; name=\"f\"\r\n"; + $chunk .= "\r\n"; + $chunk .= "u\r\n"; + $data = ''; + for ($i = 0; $i < 5000000; $i++) { + $data .= $chunk; + } + $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(); + $startTime = microtime(true); + $parser->parse($request); + $runTime = microtime(true) - $startTime; + $this->assertLessThan(1, $runTime); + } +} From 2942434617ebf896209901748f97083b454e01a0 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 2 Mar 2023 16:51:50 +0100 Subject: [PATCH 124/152] Improve multipart limits test The PR introducing this test assumed time would be enough to accurately predict behavior. This commit changes it to using introspection to check the exact state of the parser and expected state once finished parsing a multipart request body. To accomplish that it was required to make the cursor in the file an object property, so it can be inspected using reflection. --- src/Io/MultipartParser.php | 14 ++++++++------ tests/Io/MultipartParserTest.php | 29 ++++++++++++++++++++++------- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 6a874336..539107ae 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -73,6 +73,7 @@ final class MultipartParser private $postCount = 0; private $filesCount = 0; private $emptyCount = 0; + private $cursor = 0; /** * @param int|string|null $uploadMaxFilesize @@ -112,6 +113,7 @@ 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; @@ -125,20 +127,20 @@ 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/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 5dfd6e43..7f1ec667 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -1029,6 +1029,7 @@ public function testPostMaxFileSizeIgnoredByFilesComingBeforeIt() public function testWeOnlyParseTheAmountOfMultiPartChunksWeConfigured() { + $chunkCount = 5000000; $boundary = "---------------------------12758086162038677464950549563"; $chunk = "--$boundary\r\n"; @@ -1036,9 +1037,7 @@ public function testWeOnlyParseTheAmountOfMultiPartChunksWeConfigured() $chunk .= "\r\n"; $chunk .= "u\r\n"; $data = ''; - for ($i = 0; $i < 5000000; $i++) { - $data .= $chunk; - } + $data .= str_repeat($chunk, $chunkCount); $data .= "--$boundary--\r\n"; $request = new ServerRequest('POST', 'http://example.com/', array( @@ -1046,9 +1045,25 @@ public function testWeOnlyParseTheAmountOfMultiPartChunksWeConfigured() ), $data, 1.1); $parser = new MultipartParser(); - $startTime = microtime(true); - $parser->parse($request); - $runTime = microtime(true) - $startTime; - $this->assertLessThan(1, $runTime); + + $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); } } From 684421f5d09afaaa0dc1c896f9d17244544a39d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 18 Apr 2023 21:04:41 +0200 Subject: [PATCH 125/152] Enable HTTP keep-alive by default for HTTP client --- src/Browser.php | 1 - tests/BrowserTest.php | 2 -- tests/FunctionalBrowserTest.php | 53 ++++++++++++++++++++++++++++++--- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/Browser.php b/src/Browser.php index ad9187a6..b7bf4425 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -23,7 +23,6 @@ class Browser private $baseUrl; private $protocolVersion = '1.1'; private $defaultHeaders = array( - 'Connection' => 'close', 'User-Agent' => 'ReactPHP/1' ); diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index d01de9c5..b7958016 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -556,8 +556,6 @@ public function testWithMultipleHeadersShouldBeMergedCorrectlyWithMultipleDefaul 'user-Agent' => array('ABC'), 'another-header' => array('value'), 'custom-header' => array('data'), - - 'Connection' => array('close') ); $that->assertEquals($expectedHeaders, $request->getHeaders()); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 7ab909de..35b96eb6 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -553,7 +553,7 @@ public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeeps $socket->close(); } - public function testRequestWillCreateNewConnectionForSecondRequestByDefaultEvenWhenServerKeepsConnectionOpen() + public function testRequestWithConnectionCloseHeaderWillCreateNewConnectionForSecondRequestEvenWhenServerKeepsConnectionOpen() { $twice = $this->expectCallableOnce(); $socket = new SocketServer('127.0.0.1:0'); @@ -570,6 +570,9 @@ public function testRequestWillCreateNewConnectionForSecondRequestByDefaultEvenW $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()); @@ -579,12 +582,54 @@ public function testRequestWillCreateNewConnectionForSecondRequestByDefaultEvenW $this->assertEquals('hello', (string)$response->getBody()); } - public function testRequestWithoutConnectionHeaderWillReuseExistingConnectionForSecondRequest() + 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()); - // 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 . '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); From bb3154dbaf2dfe3f0467f956a05f614a69d5f1d0 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Wed, 26 Apr 2023 12:29:24 +0200 Subject: [PATCH 126/152] Prepare v1.9.0 release --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++++ README.md | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00e2d07e..d19639d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # Changelog +## 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. diff --git a/README.md b/README.md index 271f5e87..955e0a99 100644 --- a/README.md +++ b/README.md @@ -2976,7 +2976,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -composer require react/http:^1.8 +composer require react/http:^1.9 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 94222ad193ac03265da34124e5681f03e73558c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 29 Jun 2023 00:45:47 +0200 Subject: [PATCH 127/152] Update test suite to avoid unhandled promise rejections --- tests/Io/TransactionTest.php | 2 + .../LimitConcurrentRequestsMiddlewareTest.php | 45 +++++++++++++++---- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 140c53e0..284d059f 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -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() diff --git a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php index 6c63a94f..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); @@ -188,10 +191,13 @@ 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(new \RuntimeException()); @@ -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,10 +455,13 @@ 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; @@ -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', From 7a5b57c2f6e458cb8788c3692486e0756c9fc390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 7 Jul 2023 13:01:17 +0200 Subject: [PATCH 128/152] Update tests to remove defunct httpbin.org --- examples/03-client-request-any.php | 4 +- examples/04-client-post-json.php | 2 +- examples/05-client-put-xml.php | 2 +- .../22-client-stream-upload-from-stdin.php | 4 +- examples/91-client-benchmark-download.php | 2 +- tests/Client/FunctionalIntegrationTest.php | 66 ------------------- tests/FunctionalBrowserTest.php | 4 +- 7 files changed, 9 insertions(+), 75 deletions(-) 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..477c3426 100644 --- a/examples/04-client-post-json.php +++ b/examples/04-client-post-json.php @@ -16,7 +16,7 @@ ); $client->post( - 'https://httpbin.org/post', + 'https://httpbingo.org/post', array( 'Content-Type' => 'application/json' ), diff --git a/examples/05-client-put-xml.php b/examples/05-client-put-xml.php index 231e2ca4..6055363a 100644 --- a/examples/05-client-put-xml.php +++ b/examples/05-client-put-xml.php @@ -13,7 +13,7 @@ $child->name = 'Christian Lück'; $client->put( - 'https://httpbin.org/put', + 'https://httpbingo.org/put', array( 'Content-Type' => 'text/xml' ), diff --git a/examples/22-client-stream-upload-from-stdin.php b/examples/22-client-stream-upload-from-stdin.php index b00fbc5e..f29b08ab 100644 --- a/examples/22-client-stream-upload-from-stdin.php +++ b/examples/22-client-stream-upload-from-stdin.php @@ -16,10 +16,10 @@ $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) { +$client->post($url, array('Content-Type' => 'text/plain'), $in)->then(function (ResponseInterface $response) { echo 'Received' . PHP_EOL . Psr7\str($response); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; diff --git a/examples/91-client-benchmark-download.php b/examples/91-client-benchmark-download.php index 49693baf..44e99087 100644 --- a/examples/91-client-benchmark-download.php +++ b/examples/91-client-benchmark-download.php @@ -1,7 +1,7 @@ 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(new ClientConnectionManager(new Connector(), Loop::get())); - - $data = str_repeat('.', 33000); - $request = $client->request(new Request('POST', 'https://' . (mt_rand(0, 1) === 0 ? 'eu.' : '') . 'httpbin.org/post', array('Content-Length' => strlen($data)), '', '1.0')); - - $deferred = new Deferred(); - $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred) { - $deferred->resolve(Stream\buffer($body)); - }); - - $request->on('error', 'printf'); - $request->on('error', $this->expectCallableNever()); - - $request->end($data); - - $buffer = \React\Async\await(\React\Promise\Timer\timeout($deferred->promise(), 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() - { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - - $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); - - $data = json_encode(array('numbers' => range(1, 50))); - $request = $client->request(new Request('POST', 'https://httpbin.org/post', array('Content-Length' => strlen($data), 'Content-Type' => 'application/json'), '', '1.0')); - - $deferred = new Deferred(); - $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred) { - $deferred->resolve(Stream\buffer($body)); - }); - - $request->on('error', 'printf'); - $request->on('error', $this->expectCallableNever()); - - $request->end($data); - - $buffer = \React\Async\await(\React\Promise\Timer\timeout($deferred->promise(), 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 */ public function testCancelPendingConnectionEmitsClose() { diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 35b96eb6..7b8ff84b 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -230,7 +230,7 @@ public function testRequestWithAuthenticationSucceeds() /** * ```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 @@ -244,7 +244,7 @@ public function testRedirectToPageWithAuthenticationSendsAuthenticationFromLocat /** * ```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 From eb83eb06bd5ea052638266396f363fb02891cfac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 19 Sep 2023 17:45:06 +0200 Subject: [PATCH 129/152] Test on PHP 8.3 and update test environment --- .github/workflows/ci.yml | 5 +++-- composer.json | 2 +- phpunit.xml.dist | 6 +++--- phpunit.xml.legacy | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55bbaa5b..3666cd47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: php: + - 8.3 - 8.2 - 8.1 - 8.0 @@ -24,7 +25,7 @@ jobs: - 5.4 - 5.3 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} @@ -41,7 +42,7 @@ jobs: runs-on: ubuntu-22.04 continue-on-error: true steps: - - uses: actions/checkout@v3 + - 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 diff --git a/composer.json b/composer.json index 59736ddd..5198470e 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,7 @@ "clue/http-proxy-react": "^1.8", "clue/reactphp-ssh-proxy": "^1.4", "clue/socks-react": "^1.4", - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", "react/async": "^4 || ^3 || ^2", "react/promise-stream": "^1.4", "react/promise-timer": "^1.9" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7a9577e9..ac542e77 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,8 +1,8 @@ - + - + diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy index ac5600ae..89161168 100644 --- a/phpunit.xml.legacy +++ b/phpunit.xml.legacy @@ -18,7 +18,7 @@ - + From 9bf5456d95a0c4607fac0ab2f4c2926c341029d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 22 Feb 2024 15:11:43 +0100 Subject: [PATCH 130/152] Fix empty streaming request body, omit `Transfer-Encoding: chunked` --- src/Io/Sender.php | 2 +- tests/Io/SenderTest.php | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 3598d31a..1d563891 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -90,7 +90,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 { diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 3c8c4761..03a9b56e 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -5,6 +5,7 @@ use Psr\Http\Message\RequestInterface; use React\Http\Client\Client as HttpClient; 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; @@ -264,6 +265,21 @@ 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(); From 638c5dddd6f4c51c4025844ca3fbebf26e9919a9 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 17 Feb 2024 11:17:31 +0100 Subject: [PATCH 131/152] [1.x] Ensure connection close handler is cleaned up for each request This changeset resolves a small memory leak that causes roughly 1KB per connection tops. Which isn't a big issue but will make memory fluctuate more. The changeset doesn't introduce any performance degradation. Resolves: #514 Builds on top of: #405, #467, and many others --- src/Io/StreamingServer.php | 13 +++++++--- tests/Io/StreamingServerTest.php | 42 ++++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 13f0b0c4..790c8cc1 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -157,10 +157,17 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface } // cancel pending promise once connection closes + $connectionOnCloseResponseCancelerHandler = function () {}; if ($response instanceof PromiseInterface && \method_exists($response, 'cancel')) { - $conn->on('close', function () use ($response) { + $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 */ diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index a2700b86..a578797e 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -25,9 +25,17 @@ class StreamingServerTest extends TestCase */ public function setUpConnectionMockAndSocket() { - $this->connection = $this->getMockBuilder('React\Socket\Connection') + $this->connection = $this->mockConnection(); + + $this->socket = new SocketServerStub(); + } + + + private function mockConnection(array $additionalMethods = null) + { + $connection = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() - ->setMethods( + ->setMethods(array_merge( array( 'write', 'end', @@ -39,14 +47,15 @@ public function setUpConnectionMockAndSocket() 'getRemoteAddress', 'getLocalAddress', 'pipe' - ) - ) + ), + (is_array($additionalMethods) ? $additionalMethods : array()) + )) ->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() @@ -3245,6 +3254,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"; From 1bbd7f921f6a762852c9566742c20efba4672ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 18 Nov 2022 16:40:33 +0100 Subject: [PATCH 132/152] Update `Response` class to build on top of abstract message class --- src/Message/Response.php | 102 ++++++++++++++++++++++++++++--- tests/Io/StreamingServerTest.php | 4 +- tests/Message/ResponseTest.php | 33 ++++++++++ 3 files changed, 128 insertions(+), 11 deletions(-) diff --git a/src/Message/Response.php b/src/Message/Response.php index edd6245b..c50d0cee 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\BufferedBody; use React\Http\Io\HttpBodyStream; use React\Stream\ReadableStreamInterface; -use RingCentral\Psr7\Response as Psr7Response; +use RingCentral\Psr7\MessageTrait; /** * Represents an outgoing server response message. @@ -40,7 +41,7 @@ * * @see \Psr\Http\Message\ResponseInterface */ -final class Response extends Psr7Response implements StatusCodeInterface +final class Response extends MessageTrait implements ResponseInterface, StatusCodeInterface { /** * Create an HTML response @@ -257,6 +258,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 +316,60 @@ public function __construct( throw new \InvalidArgumentException('Invalid response body given'); } - parent::__construct( - $status, - $headers, - $body, - $version, - $reason - ); + $this->protocol = (string) $version; + $this->setHeaders($headers); + $this->stream = $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] : ''; } } diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index a578797e..64566ddc 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -1567,9 +1567,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() diff --git a/tests/Message/ResponseTest.php b/tests/Message/ResponseTest.php index ed21cdc2..88b56945 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() { From 518ca68ca8f03f61e9e662dedb1ce2383307a677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 15 Sep 2023 20:08:07 +0200 Subject: [PATCH 133/152] Add internal `AbstractMessage` base class (PSR-7) --- README.md | 3 +- src/Io/AbstractMessage.php | 164 +++++++++++++++++ src/Message/Response.php | 11 +- tests/Io/AbstractMessageTest.php | 222 ++++++++++++++++++++++++ tests/Io/MiddlewareRunnerTest.php | 2 +- tests/Message/ResponseExceptionTest.php | 2 +- 6 files changed, 393 insertions(+), 11 deletions(-) create mode 100644 src/Io/AbstractMessage.php create mode 100644 tests/Io/AbstractMessageTest.php diff --git a/README.md b/README.md index 955e0a99..31d0430a 100644 --- a/README.md +++ b/README.md @@ -2448,8 +2448,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() diff --git a/src/Io/AbstractMessage.php b/src/Io/AbstractMessage.php new file mode 100644 index 00000000..8523d6cd --- /dev/null +++ b/src/Io/AbstractMessage.php @@ -0,0 +1,164 @@ + */ + 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/Message/Response.php b/src/Message/Response.php index c50d0cee..95c82ec8 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -5,10 +5,10 @@ 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\MessageTrait; /** * Represents an outgoing server response message. @@ -35,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 MessageTrait implements ResponseInterface, StatusCodeInterface +final class Response extends AbstractMessage implements ResponseInterface, StatusCodeInterface { /** * Create an HTML response @@ -316,9 +315,7 @@ public function __construct( throw new \InvalidArgumentException('Invalid response body given'); } - $this->protocol = (string) $version; - $this->setHeaders($headers); - $this->stream = $body; + parent::__construct($version, $headers, $body); $this->statusCode = (int) $status; $this->reasonPhrase = ($reason !== '' && $reason !== null) ? (string) $reason : self::getReasonPhraseForStatusCode($status); 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/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index ac836f03..762d7bdb 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -6,12 +6,12 @@ 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\PromiseInterface; use React\Tests\Http\Middleware\ProcessStack; use React\Tests\Http\TestCase; -use RingCentral\Psr7\Response; final class MiddlewareRunnerTest extends TestCase { 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 { From 3313e1fb7c9a47416458944f7988db3f476892fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 12 Mar 2024 15:24:13 +0100 Subject: [PATCH 134/152] Add internal `AbstractRequest` base class for `Request` class (PSR-7) --- README.md | 3 +- src/Io/AbstractRequest.php | 156 +++++++++++ src/Message/Request.php | 7 +- tests/Io/AbstractRequestTest.php | 449 +++++++++++++++++++++++++++++++ 4 files changed, 609 insertions(+), 6 deletions(-) create mode 100644 src/Io/AbstractRequest.php create mode 100644 tests/Io/AbstractRequestTest.php diff --git a/README.md b/README.md index 31d0430a..067e5b9f 100644 --- a/README.md +++ b/README.md @@ -2642,8 +2642,7 @@ 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 an existing outgoing - request message and only adds support for streaming. 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. #### ServerRequest diff --git a/src/Io/AbstractRequest.php b/src/Io/AbstractRequest.php new file mode 100644 index 00000000..51059ac5 --- /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/Message/Request.php b/src/Message/Request.php index cf59641e..3de8c1b3 100644 --- a/src/Message/Request.php +++ b/src/Message/Request.php @@ -5,10 +5,10 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; +use React\Http\Io\AbstractRequest; use React\Http\Io\BufferedBody; use React\Http\Io\ReadableBodyStream; use React\Stream\ReadableStreamInterface; -use RingCentral\Psr7\Request as BaseRequest; /** * Respresents an outgoing HTTP request message. @@ -22,13 +22,12 @@ * 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 an existing outgoing - * request message and only adds support for streaming. 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 RequestInterface */ -final class Request extends BaseRequest implements RequestInterface +final class Request extends AbstractRequest implements RequestInterface { /** * @param string $method HTTP method for the request. diff --git a/tests/Io/AbstractRequestTest.php b/tests/Io/AbstractRequestTest.php new file mode 100644 index 00000000..28c9eaf1 --- /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()); + } +} From 0638dcdbea657c3226b90dfadd1737d706ca84e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 12 Mar 2024 18:20:42 +0100 Subject: [PATCH 135/152] Update `ServerRequest` class to build on top of abstract request class --- README.md | 3 +-- src/Message/ServerRequest.php | 25 ++++++++++--------------- tests/Io/StreamingServerTest.php | 32 ++++++++++++++++---------------- 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 067e5b9f..47003770 100644 --- a/README.md +++ b/README.md @@ -2661,8 +2661,7 @@ 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. #### ResponseException diff --git a/src/Message/ServerRequest.php b/src/Message/ServerRequest.php index 25532cf4..b5c41413 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 as BaseRequest; /** * 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 BaseRequest 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 !== '') { diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 64566ddc..afab371e 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -126,7 +126,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()); @@ -159,7 +159,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()); @@ -182,7 +182,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()); @@ -204,7 +204,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()); @@ -226,7 +226,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()); @@ -248,7 +248,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()); @@ -283,7 +283,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()); @@ -316,7 +316,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()); @@ -338,7 +338,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()); @@ -360,7 +360,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()); @@ -382,7 +382,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()); @@ -434,7 +434,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()); @@ -455,7 +455,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()); @@ -477,7 +477,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()); @@ -511,7 +511,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()); @@ -533,7 +533,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()); From 5896f81bbd6a024581c08330ca619060f23656a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 15 Mar 2024 17:19:54 +0100 Subject: [PATCH 136/152] Move parsing incoming HTTP request message to `ServerRequest` --- src/Io/AbstractMessage.php | 8 ++ src/Io/RequestHeaderParser.php | 130 +------------------------- src/Message/ServerRequest.php | 139 ++++++++++++++++++++++++++++ tests/Message/ServerRequestTest.php | 122 ++++++++++++++++++++++++ 4 files changed, 270 insertions(+), 129 deletions(-) diff --git a/src/Io/AbstractMessage.php b/src/Io/AbstractMessage.php index 8523d6cd..ab023304 100644 --- a/src/Io/AbstractMessage.php +++ b/src/Io/AbstractMessage.php @@ -13,6 +13,14 @@ */ abstract class AbstractMessage implements MessageInterface { + /** + * [Internal] Regex used to match all request header fields into an array, thanks to @kelunik for checking the HTTP specs and coming up with this regex + * + * @internal + * @var string + */ + const REGEX_HEADERS = '/^([^()<>@,;:\\\"\/\[\]?={}\x01-\x20\x7F]++):[\x20\x09]*+((?:[\x20\x09]*+[\x21-\x7E\x80-\xFF]++)*+)[\x20\x09]*+[\r]?+\n/m'; + /** @var array */ private $headers = array(); diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index b8336f5b..8975ce57 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -128,39 +128,6 @@ public function handle(ConnectionInterface $conn) */ 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])) { @@ -207,101 +174,6 @@ public function parseRequest($headers, ConnectionInterface $connection) $serverParams['REQUEST_TIME'] = (int) ($now = $this->clock->now()); $serverParams['REQUEST_TIME_FLOAT'] = $now; - // 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 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); - } - } - - return $request; + return ServerRequest::parseMessage($headers, $serverParams); } } diff --git a/src/Message/ServerRequest.php b/src/Message/ServerRequest.php index b5c41413..32a0f62f 100644 --- a/src/Message/ServerRequest.php +++ b/src/Message/ServerRequest.php @@ -189,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/tests/Message/ServerRequestTest.php b/tests/Message/ServerRequestTest.php index a5919f64..f82d60f8 100644 --- a/tests/Message/ServerRequestTest.php +++ b/tests/Message/ServerRequestTest.php @@ -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()); + } } From c0e1f4d90b27b57b839a2cc2fa8ca315d39d8c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 21 Mar 2024 15:08:15 +0100 Subject: [PATCH 137/152] Move parsing incoming HTTP response message to `Response` --- src/Io/ClientRequestStream.php | 14 +++-- src/Message/Response.php | 42 +++++++++++++++ tests/Message/ResponseTest.php | 94 ++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 4 deletions(-) diff --git a/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index 0220f008..25c96ea8 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -8,7 +8,6 @@ use React\Http\Message\Response; use React\Socket\ConnectionInterface; use React\Stream\WritableStreamInterface; -use RingCentral\Psr7 as gPsr; /** * @event response @@ -152,10 +151,17 @@ 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")) { + $eom = \strpos($this->buffer, "\r\n\r\n"); + $eomLegacy = \strpos($this->buffer, "\n\n"); + if ($eom !== false || $eomLegacy !== false) { try { - $response = gPsr\parse_response($this->buffer); - $bodyChunk = (string) $response->getBody(); + 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; diff --git a/src/Message/Response.php b/src/Message/Response.php index 95c82ec8..fa6366ed 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -369,4 +369,46 @@ private static function getReasonPhraseForStatusCode($code) 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, + '', + $start['version'], + isset($start['reason']) ? $start['reason'] : '' + ); + } } diff --git a/tests/Message/ResponseTest.php b/tests/Message/ResponseTest.php index 88b56945..a9a244c2 100644 --- a/tests/Message/ResponseTest.php +++ b/tests/Message/ResponseTest.php @@ -157,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"); + } } From a73e9f78ab31b3e8876371236f26e9d559ea59c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 14 Mar 2024 11:43:50 +0100 Subject: [PATCH 138/152] Add new `Uri` class for new PSR-7 implementation --- README.md | 13 + src/Io/AbstractRequest.php | 2 +- src/Message/Uri.php | 292 ++++++++++++ tests/BrowserTest.php | 1 - tests/Io/AbstractRequestTest.php | 2 +- tests/Io/ClientConnectionManagerTest.php | 2 +- tests/Io/ClientRequestStreamTest.php | 2 +- tests/Message/UriTest.php | 581 +++++++++++++++++++++++ 8 files changed, 890 insertions(+), 5 deletions(-) create mode 100644 src/Message/Uri.php create mode 100644 tests/Message/UriTest.php diff --git a/README.md b/README.md index 47003770..18089464 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ multiple concurrent HTTP requests without blocking. * [xml()](#xml) * [Request](#request-1) * [ServerRequest](#serverrequest) + * [Uri](#uri) * [ResponseException](#responseexception) * [React\Http\Middleware](#reacthttpmiddleware) * [StreamingRequestMiddleware](#streamingrequestmiddleware) @@ -2664,6 +2665,18 @@ application reacts to certain HTTP requests. > 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 diff --git a/src/Io/AbstractRequest.php b/src/Io/AbstractRequest.php index 51059ac5..f32307f7 100644 --- a/src/Io/AbstractRequest.php +++ b/src/Io/AbstractRequest.php @@ -5,7 +5,7 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; -use RingCentral\Psr7\Uri; +use React\Http\Message\Uri; /** * [Internal] Abstract HTTP request base class (PSR-7) diff --git a/src/Message/Uri.php b/src/Message/Uri.php new file mode 100644 index 00000000..f2cf7d99 --- /dev/null +++ b/src/Message/Uri.php @@ -0,0 +1,292 @@ +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 + ); + } +} diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index b7958016..fdd338d9 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -5,7 +5,6 @@ use Psr\Http\Message\RequestInterface; use React\Http\Browser; use React\Promise\Promise; -use RingCentral\Psr7\Uri; class BrowserTest extends TestCase { diff --git a/tests/Io/AbstractRequestTest.php b/tests/Io/AbstractRequestTest.php index 28c9eaf1..7ff4a9a5 100644 --- a/tests/Io/AbstractRequestTest.php +++ b/tests/Io/AbstractRequestTest.php @@ -5,8 +5,8 @@ use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; use React\Http\Io\AbstractRequest; +use React\Http\Message\Uri; use React\Tests\Http\TestCase; -use RingCentral\Psr7\Uri; class RequestMock extends AbstractRequest { diff --git a/tests/Io/ClientConnectionManagerTest.php b/tests/Io/ClientConnectionManagerTest.php index b28c7964..6aafa6db 100644 --- a/tests/Io/ClientConnectionManagerTest.php +++ b/tests/Io/ClientConnectionManagerTest.php @@ -2,8 +2,8 @@ namespace React\Tests\Http\Io; -use RingCentral\Psr7\Uri; use React\Http\Io\ClientConnectionManager; +use React\Http\Message\Uri; use React\Promise\Promise; use React\Promise\PromiseInterface; use React\Tests\Http\TestCase; diff --git a/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index 4649087a..181db173 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -3,9 +3,9 @@ namespace React\Tests\Http\Io; use Psr\Http\Message\ResponseInterface; -use RingCentral\Psr7\Uri; use React\Http\Io\ClientRequestStream; use React\Http\Message\Request; +use React\Http\Message\Uri; use React\Promise\Deferred; use React\Promise\Promise; use React\Stream\DuplexResourceStream; diff --git a/tests/Message/UriTest.php b/tests/Message/UriTest.php new file mode 100644 index 00000000..95c7fa4e --- /dev/null +++ b/tests/Message/UriTest.php @@ -0,0 +1,581 @@ +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' + ) + ); + } + + /** + * @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 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()); + } +} From c6caa1240307f13e7a678332ad0beae7fb909160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 23 Mar 2024 17:15:23 +0100 Subject: [PATCH 139/152] Add internal `Uri::resolve()` to resolve URIs relative to base URI --- src/Browser.php | 4 +- src/Io/Transaction.php | 4 +- src/Message/Uri.php | 64 ++++++++++++++++++++ tests/Message/UriTest.php | 124 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 192 insertions(+), 4 deletions(-) diff --git a/src/Browser.php b/src/Browser.php index b7bf4425..01a266ca 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -3,12 +3,12 @@ namespace React\Http; use Psr\Http\Message\ResponseInterface; -use RingCentral\Psr7\Uri; use React\EventLoop\Loop; use React\EventLoop\LoopInterface; 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\ConnectorInterface; use React\Stream\ReadableStreamInterface; @@ -834,7 +834,7 @@ private function requestMayBeStreaming($method, $url, array $headers = array(), { 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)); } foreach ($this->defaultHeaders as $key => $value) { diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index b93c490c..64738f56 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -8,11 +8,11 @@ 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; -use RingCentral\Psr7\Uri; /** * @internal @@ -264,7 +264,7 @@ public function onResponse(ResponseInterface $response, RequestInterface $reques 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, $response->getStatusCode()); $this->progress('redirect', array($request)); diff --git a/src/Message/Uri.php b/src/Message/Uri.php index f2cf7d99..4309bbed 100644 --- a/src/Message/Uri.php +++ b/src/Message/Uri.php @@ -289,4 +289,68 @@ function (array $match) { $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/tests/Message/UriTest.php b/tests/Message/UriTest.php index 95c7fa4e..05eec723 100644 --- a/tests/Message/UriTest.php +++ b/tests/Message/UriTest.php @@ -578,4 +578,128 @@ public function testWithFragmentReturnsSameInstanceWhenFragmentIsUnchangedEncode $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); + } } From 30c802c9db77d90dc0d6ef43d9a60573863a0969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 23 Mar 2024 17:42:14 +0100 Subject: [PATCH 140/152] Drop leftover RingCentral PSR-7 dependency, use own PSR-7 implementation --- composer.json | 3 +-- examples/01-client-get-request.php | 2 +- examples/02-client-concurrent-requests.php | 6 +++--- examples/04-client-post-json.php | 2 +- examples/05-client-put-xml.php | 2 +- examples/11-client-http-proxy.php | 2 +- examples/12-client-socks-proxy.php | 2 +- examples/13-client-ssh-proxy.php | 2 +- examples/14-client-unix-domain-sockets.php | 3 +-- examples/21-client-request-streaming-to-stdout.php | 3 +-- examples/22-client-stream-upload-from-stdin.php | 3 +-- examples/71-server-http-proxy.php | 2 +- examples/91-client-benchmark-download.php | 3 +-- src/Io/ClientRequestStream.php | 3 ++- tests/Middleware/RequestBodyBufferMiddlewareTest.php | 5 ++--- 15 files changed, 19 insertions(+), 24 deletions(-) diff --git a/composer.json b/composer.json index 5198470e..23783c0c 100644 --- a/composer.json +++ b/composer.json @@ -33,8 +33,7 @@ "react/event-loop": "^1.2", "react/promise": "^3 || ^2.3 || ^1.2.1", "react/socket": "^1.12", - "react/stream": "^1.2", - "ringcentral/psr7": "^1.2" + "react/stream": "^1.2" }, "require-dev": { "clue/http-proxy-react": "^1.8", 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/04-client-post-json.php b/examples/04-client-post-json.php index 477c3426..18fa596d 100644 --- a/examples/04-client-post-json.php +++ b/examples/04-client-post-json.php @@ -22,7 +22,7 @@ ), 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 6055363a..10ee46fc 100644 --- a/examples/05-client-put-xml.php +++ b/examples/05-client-put-xml.php @@ -19,7 +19,7 @@ ), $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 f29b08ab..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'; @@ -20,7 +19,7 @@ echo 'Sending STDIN as POST to ' . $url . '…' . PHP_EOL; $client->post($url, array('Content-Type' => 'text/plain'), $in)->then(function (ResponseInterface $response) { - echo 'Received' . PHP_EOL . Psr7\str($response); + echo (string) $response->getBody(); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); 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 44e99087..712d9f10 100644 --- a/examples/91-client-benchmark-download.php +++ b/examples/91-client-benchmark-download.php @@ -29,8 +29,7 @@ echo 'Requesting ' . $url . '…' . PHP_EOL; $client->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/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index 25c96ea8..12c15caf 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -277,7 +277,8 @@ public function close() */ public function hasMessageKeepAliveEnabled(MessageInterface $message) { - $connectionOptions = \RingCentral\Psr7\normalize_header(\strtolower($message->getHeaderLine('Connection'))); + // @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; diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index fd818a8c..40c23378 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -4,13 +4,13 @@ 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 { @@ -45,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/', From 27d2e74c0626acb0f6f504d8ba25632036b79818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 27 Mar 2024 10:48:52 +0100 Subject: [PATCH 141/152] Validate outgoing HTTP message headers and reject invalid messages --- src/Io/AbstractMessage.php | 2 +- src/Io/ClientRequestStream.php | 30 ++++++---- src/Io/StreamingServer.php | 13 ++++ tests/Io/ClientRequestStreamTest.php | 62 ++++++++++++++++++++ tests/Io/StreamingServerTest.php | 88 +++++++++++++++++++++++++++- 5 files changed, 183 insertions(+), 12 deletions(-) diff --git a/src/Io/AbstractMessage.php b/src/Io/AbstractMessage.php index ab023304..a0706bb1 100644 --- a/src/Io/AbstractMessage.php +++ b/src/Io/AbstractMessage.php @@ -19,7 +19,7 @@ abstract class AbstractMessage implements MessageInterface * @internal * @var string */ - const REGEX_HEADERS = '/^([^()<>@,;:\\\"\/\[\]?={}\x01-\x20\x7F]++):[\x20\x09]*+((?:[\x20\x09]*+[\x21-\x7E\x80-\xFF]++)*+)[\x20\x09]*+[\r]?+\n/m'; + const REGEX_HEADERS = '/^([^()<>@,;:\\\"\/\[\]?={}\x00-\x20\x7F]++):[\x20\x09]*+((?:[\x20\x09]*+[\x21-\x7E\x80-\xFF]++)*+)[\x20\x09]*+[\r]?+\n/m'; /** @var array */ private $headers = array(); diff --git a/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index 25c96ea8..ee0ec760 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -56,7 +56,25 @@ private function writeHead() { $this->state = self::STATE_WRITING_HEAD; - $request = $this->request; + $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; @@ -64,7 +82,7 @@ private function writeHead() $promise = $this->connectionManager->connect($this->request->getUri()); $promise->then( - function (ConnectionInterface $connection) use ($request, &$connectionRef, &$stateRef, &$pendingWrites, $that) { + function (ConnectionInterface $connection) use ($headers, &$connectionRef, &$stateRef, &$pendingWrites, $that) { $connectionRef = $connection; assert($connectionRef instanceof ConnectionInterface); @@ -74,14 +92,6 @@ function (ConnectionInterface $connection) use ($request, &$connectionRef, &$sta $connection->on('error', array($that, 'handleError')); $connection->on('close', array($that, 'close')); - assert($request instanceof RequestInterface); - $headers = "{$request->getMethod()} {$request->getRequestTarget()} HTTP/{$request->getProtocolVersion()}\r\n"; - foreach ($request->getHeaders() as $name => $values) { - foreach ($values as $value) { - $headers .= "$name: $value\r\n"; - } - } - $more = $connection->write($headers . "\r\n" . $pendingWrites); assert($stateRef === ClientRequestStream::STATE_WRITING_HEAD); diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 790c8cc1..143edaa8 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -333,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/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index 181db173..9a5373a1 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -2,6 +2,7 @@ namespace React\Tests\Http\Io; +use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use React\Http\Io\ClientRequestStream; use React\Http\Message\Request; @@ -100,6 +101,67 @@ public function requestShouldEmitErrorIfConnectionEmitsError() $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() { diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index afab371e..b4e3f2f8 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; @@ -2511,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() @@ -2926,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; From 8111281ee57f22b7194f5dba225e609ba7ce4d20 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Wed, 27 Mar 2024 18:20:46 +0100 Subject: [PATCH 142/152] Prepare v1.10.0 release --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ README.md | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d19639d8..f69779c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## 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. diff --git a/README.md b/README.md index 18089464..d0550642 100644 --- a/README.md +++ b/README.md @@ -2986,7 +2986,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -composer require react/http:^1.9 +composer require react/http:^1.10 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From dfbeef0fc47d91eeab88e2a38566e18fa6633f1f Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 16 May 2024 12:10:12 +0200 Subject: [PATCH 143/152] Hello `3.x` development branch Once this PR is merged, we can start working on the new [v3.0.0 milestone](https://github.com/reactphp/http/milestone/37). The default branch will be `3.x` and the old `1.x` branch still stay in place at least until `3.0.0` is released. Refs: Road map ticket for http: #517 Plans for ReactPHP v3: https://github.com/orgs/reactphp/discussions/481 PR templated from: https://github.com/friends-of-reactphp/mysql/pull/185 --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d0550642..8867f798 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,14 @@ Event-driven, streaming HTTP client and server implementation for [ReactPHP](https://reactphp.org/). +> **Development version:** This branch contains the code for the upcoming v3 +> release. For the code of the current stable v1 release, check out the +> [`1.x` branch](https://github.com/reactphp/http/tree/1.x). +> +> The upcoming v3 release will be the way forward for this package. However, +> we will still actively support v1 for those not yet on the latest version. +> See also [installation instructions](#install) for more details. + This HTTP library provides re-usable implementations for an HTTP client and server based on ReactPHP's [`Socket`](https://github.com/reactphp/socket) and [`EventLoop`](https://github.com/reactphp/event-loop) components. @@ -2982,11 +2990,11 @@ new React\Http\Middleware\RequestBodyParserMiddleware(10 * 1024, 100); // 100 fi The recommended way to install this library is [through Composer](https://getcomposer.org/). [New to Composer?](https://getcomposer.org/doc/00-intro.md) -This project follows [SemVer](https://semver.org/). -This will install the latest supported version: +Once released, this project will follow [SemVer](https://semver.org/). +At the moment, this will install the latest development version: ```bash -composer require react/http:^1.10 +composer require react/http:^3@dev ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 53f935e5fa788e92d98e137ff5e52116650bd708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 28 Sep 2023 19:29:35 +0200 Subject: [PATCH 144/152] Drop deprecated `Server` class, use `HttpServer` instead --- README.md | 6 ------ src/HttpServer.php | 4 ---- src/Server.php | 18 ------------------ tests/ServerTest.php | 15 --------------- 4 files changed, 43 deletions(-) delete mode 100644 src/Server.php delete mode 100644 tests/ServerTest.php diff --git a/README.md b/README.md index 8867f798..8e3f29a2 100644 --- a/README.md +++ b/README.md @@ -740,8 +740,6 @@ See also the [Unix Domain Sockets (UDS) example](examples/14-client-unix-domain- ### HttpServer - - The `React\Http\HttpServer` class is responsible for handling incoming connections and then processing each incoming HTTP request. @@ -891,10 +889,6 @@ have full control over consuming the incoming HTTP request body and concurrency settings. See also [streaming incoming request](#streaming-incoming-request) below for more details. -> Changelog v1.5.0: This class has been renamed to `HttpServer` from the - previous `Server` class in order to avoid any ambiguities. - The previous name has been deprecated and should not be used anymore. - ### listen() The `listen(React\Socket\ServerInterface $socket): void` method can be used to diff --git a/src/HttpServer.php b/src/HttpServer.php index f2334733..cd0874cc 100644 --- a/src/HttpServer.php +++ b/src/HttpServer.php @@ -167,10 +167,6 @@ * have full control over consuming the incoming HTTP request body and * concurrency settings. See also [streaming incoming request](#streaming-incoming-request) * below for more details. - * - * > Changelog v1.5.0: This class has been renamed to `HttpServer` from the - * previous `Server` class in order to avoid any ambiguities. - * The previous name has been deprecated and should not be used anymore. */ final class HttpServer extends EventEmitter { diff --git a/src/Server.php b/src/Server.php deleted file mode 100644 index 9bb9cf7f..00000000 --- a/src/Server.php +++ /dev/null @@ -1,18 +0,0 @@ -assertInstanceOf('React\Http\HttpServer', $http); - } -} From 6956fe2de8f37c1027fb3936d829ab66e3e9da38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 29 Sep 2023 12:25:11 +0200 Subject: [PATCH 145/152] Drop deprecated alternative `Browser` constructor argument order --- README.md | 4 --- src/Browser.php | 22 ++----------- tests/BrowserTest.php | 72 ------------------------------------------- 3 files changed, 3 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index 8867f798..2f7151ac 100644 --- a/README.md +++ b/README.md @@ -1869,11 +1869,7 @@ $browser = new React\Http\Browser(); This class takes two optional arguments for more advanced usage: ```php -// constructor signature as of v1.5.0 $browser = new React\Http\Browser(?ConnectorInterface $connector = null, ?LoopInterface $loop = null); - -// legacy constructor signature before v1.5.0 -$browser = new React\Http\Browser(?LoopInterface $loop = null, ?ConnectorInterface $connector = null); ``` If you need custom connector settings (DNS resolution, TLS parameters, timeouts, diff --git a/src/Browser.php b/src/Browser.php index 01a266ca..a24d24e7 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -37,11 +37,7 @@ class Browser * This class takes two optional arguments for more advanced usage: * * ```php - * // constructor signature as of v1.5.0 * $browser = new React\Http\Browser(?ConnectorInterface $connector = null, ?LoopInterface $loop = null); - * - * // legacy constructor signature before v1.5.0 - * $browser = new React\Http\Browser(?LoopInterface $loop = null, ?ConnectorInterface $connector = null); * ``` * * If you need custom connector settings (DNS resolution, TLS parameters, timeouts, @@ -69,23 +65,11 @@ class Browser * This value SHOULD NOT be given unless you're sure you want to explicitly use a * given event loop instance. * - * @param null|ConnectorInterface|LoopInterface $connector - * @param null|LoopInterface|ConnectorInterface $loop - * @throws \InvalidArgumentException for invalid arguments + * @param ?ConnectorInterface $connector + * @param ?LoopInterface $loop */ - public function __construct($connector = null, $loop = null) + public function __construct(ConnectorInterface $connector = null, LoopInterface $loop = null) { - // swap arguments for legacy constructor signature - if (($connector instanceof LoopInterface || $connector === null) && ($loop instanceof ConnectorInterface || $loop === null)) { - $swap = $loop; - $loop = $connector; - $connector = $swap; - } - - if (($connector !== null && !$connector instanceof ConnectorInterface) || ($loop !== null && !$loop instanceof LoopInterface)) { - throw new \InvalidArgumentException('Expected "?ConnectorInterface $connector" and "?LoopInterface $loop" arguments'); - } - $loop = $loop ?: Loop::get(); $this->transaction = new Transaction( Sender::createFromLoop($loop, $connector), diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index fdd338d9..fb1a1beb 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -70,35 +70,6 @@ public function testConstructWithConnectorAssignsGivenConnector() $this->assertSame($connector, $ret); } - public function testConstructWithConnectorWithLegacySignatureAssignsGivenConnector() - { - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); - - $browser = new Browser(null, $connector); - - $ref = new \ReflectionProperty($browser, 'transaction'); - $ref->setAccessible(true); - $transaction = $ref->getValue($browser); - - $ref = new \ReflectionProperty($transaction, 'sender'); - $ref->setAccessible(true); - $sender = $ref->getValue($transaction); - - $ref = new \ReflectionProperty($sender, 'http'); - $ref->setAccessible(true); - $client = $ref->getValue($sender); - - $ref = new \ReflectionProperty($client, 'connectionManager'); - $ref->setAccessible(true); - $connectionManager = $ref->getValue($client); - - $ref = new \ReflectionProperty($connectionManager, 'connector'); - $ref->setAccessible(true); - $ret = $ref->getValue($connectionManager); - - $this->assertSame($connector, $ret); - } - public function testConstructWithLoopAssignsGivenLoop() { $browser = new Browser(null, $this->loop); @@ -114,49 +85,6 @@ public function testConstructWithLoopAssignsGivenLoop() $this->assertSame($this->loop, $loop); } - public function testConstructWithLoopWithLegacySignatureAssignsGivenLoop() - { - $browser = new Browser($this->loop); - - $ref = new \ReflectionProperty($browser, 'transaction'); - $ref->setAccessible(true); - $transaction = $ref->getValue($browser); - - $ref = new \ReflectionProperty($transaction, 'loop'); - $ref->setAccessible(true); - $loop = $ref->getValue($transaction); - - $this->assertSame($this->loop, $loop); - } - - public function testConstructWithInvalidConnectorThrows() - { - $this->setExpectedException('InvalidArgumentException'); - new Browser('foo'); - } - - public function testConstructWithInvalidLoopThrows() - { - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); - - $this->setExpectedException('InvalidArgumentException'); - new Browser($connector, 'foo'); - } - - public function testConstructWithConnectorTwiceThrows() - { - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); - - $this->setExpectedException('InvalidArgumentException'); - new Browser($connector, $connector); - } - - public function testConstructWithLoopTwiceThrows() - { - $this->setExpectedException('InvalidArgumentException'); - new Browser($this->loop, $this->loop); - } - public function testGetSendsGetRequest() { $that = $this; From acd2e1401a96fee53a971a51499de9fb0c7c26c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 May 2024 12:58:26 +0200 Subject: [PATCH 146/152] Update to require PHP 7.1+ --- .github/workflows/ci.yml | 21 ----------- README.md | 3 +- composer.json | 6 ++-- phpunit.xml.legacy | 2 +- src/Io/ClientRequestStream.php | 3 +- src/Io/MultipartParser.php | 14 +++----- src/Io/StreamingServer.php | 3 +- src/Message/Response.php | 3 +- src/Message/Uri.php | 11 +----- tests/Client/FunctionalIntegrationTest.php | 6 ---- tests/FunctionalBrowserTest.php | 12 ------- tests/FunctionalHttpServerTest.php | 28 --------------- tests/HttpServerTest.php | 3 -- tests/Io/MiddlewareRunnerTest.php | 3 -- tests/Io/RequestHeaderParserTest.php | 4 --- tests/Io/StreamingServerTest.php | 6 ---- tests/Message/ResponseTest.php | 12 +------ tests/Message/UriTest.php | 5 --- .../LimitConcurrentRequestsMiddlewareTest.php | 3 -- .../RequestBodyBufferMiddlewareTest.php | 3 -- .../RequestBodyParserMiddlewareTest.php | 35 ++----------------- tests/TestCase.php | 18 ++++------ 22 files changed, 23 insertions(+), 181 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3666cd47..6f9cfb41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,11 +19,6 @@ jobs: - 7.3 - 7.2 - 7.1 - - 7.0 - - 5.6 - - 5.5 - - 5.4 - - 5.3 steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 @@ -36,19 +31,3 @@ jobs: if: ${{ matrix.php >= 7.3 }} - run: vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy if: ${{ matrix.php < 7.3 }} - - PHPUnit-hhvm: - name: PHPUnit (HHVM) - runs-on: ubuntu-22.04 - continue-on-error: true - steps: - - 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: - 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/README.md b/README.md index 46da303d..9cdf7c09 100644 --- a/README.md +++ b/README.md @@ -2990,8 +2990,7 @@ composer require react/http:^3@dev 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.3 through current PHP 8+ and -HHVM. +extensions and supports running on PHP 7.1 through current PHP 8+. It's *highly recommended to use the latest supported PHP version* for this project. ## Tests diff --git a/composer.json b/composer.json index 23783c0c..33919186 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ } ], "require": { - "php": ">=5.3.0", + "php": ">=7.1", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "fig/http-message-util": "^1.1", "psr/http-message": "^1.0", @@ -39,8 +39,8 @@ "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 || ^3 || ^2", + "phpunit/phpunit": "^9.6 || ^5.7", + "react/async": "^4 || ^3", "react/promise-stream": "^1.4", "react/promise-timer": "^1.9" }, diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy index 89161168..a018d7ab 100644 --- a/phpunit.xml.legacy +++ b/phpunit.xml.legacy @@ -2,7 +2,7 @@ diff --git a/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index ff9bf2d4..3bdf9b1f 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -69,8 +69,7 @@ private function writeHead() } } - /** @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) { + if (!\preg_match('#^\S+ \S+ HTTP/1\.[01]\r\n#m', $headers) || \substr_count($headers, "\n") !== ($expected + 1) || \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers) !== $expected) { $this->closeError(new \InvalidArgumentException('Unable to send request with invalid request headers')); return; } diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index 539107ae..c65bb655 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -36,7 +36,7 @@ final class MultipartParser /** * ini setting "max_input_vars" * - * Does not exist in PHP < 5.3.9 or HHVM, so assume PHP's default 1000 here. + * Assume PHP' default of 1000 here. * * @var int * @link http://php.net/manual/en/info.configuration.php#ini.max-input-vars @@ -46,7 +46,7 @@ final class MultipartParser /** * ini setting "max_input_nesting_level" * - * Does not exist in HHVM, but assumes hard coded to 64 (PHP's default). + * Assume PHP's default of 64 here. * * @var int * @link http://php.net/manual/en/info.configuration.php#ini.max-input-nesting-level @@ -81,14 +81,8 @@ final class MultipartParser */ public function __construct($uploadMaxFilesize = null, $maxFileUploads = null) { - $var = \ini_get('max_input_vars'); - if ($var !== false) { - $this->maxInputVars = (int)$var; - } - $var = \ini_get('max_input_nesting_level'); - if ($var !== false) { - $this->maxInputNestingLevel = (int)$var; - } + $this->maxInputVars = (int) \ini_get('max_input_vars'); + $this->maxInputNestingLevel = (int) \ini_get('max_input_nesting_level'); if ($uploadMaxFilesize === null) { $uploadMaxFilesize = \ini_get('upload_max_filesize'); diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index 143edaa8..eee9f900 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -346,8 +346,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt } } - /** @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) { + if ($code < 100 || $code > 999 || \substr_count($headers, "\n") !== ($expected + 1) || \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers) !== $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; diff --git a/src/Message/Response.php b/src/Message/Response.php index fa6366ed..107508a9 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -151,8 +151,7 @@ public static function json($data) (\defined('JSON_PRETTY_PRINT') ? \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE : 0) | (\defined('JSON_PRESERVE_ZERO_FRACTION') ? \JSON_PRESERVE_ZERO_FRACTION : 0) ); - // throw on error, now `false` but used to be `(string) "null"` before PHP 5.5 - if ($json === false || (\PHP_VERSION_ID < 50500 && \json_last_error() !== \JSON_ERROR_NONE)) { + if ($json === false) { throw new \InvalidArgumentException( 'Unable to encode given data as JSON' . (\function_exists('json_last_error_msg') ? ': ' . \json_last_error_msg() : ''), \json_last_error() diff --git a/src/Message/Uri.php b/src/Message/Uri.php index 4309bbed..6b77d3e7 100644 --- a/src/Message/Uri.php +++ b/src/Message/Uri.php @@ -45,16 +45,7 @@ final class Uri implements UriInterface */ public function __construct($uri) { - // @codeCoverageIgnoreStart - if (\PHP_VERSION_ID < 50407 && \strpos($uri, '//') === 0) { - // @link https://3v4l.org/UrAQP - $parts = \parse_url('http:' . $uri); - unset($parts['schema']); - } else { - $parts = \parse_url(https://github.com/$uri); - } - // @codeCoverageIgnoreEnd - + $parts = \parse_url(https://github.com/$uri); if ($parts === false || (isset($parts['scheme']) && !\preg_match('#^[a-z]+$#i', $parts['scheme'])) || (isset($parts['host']) && \preg_match('#[\s_%+]#', $parts['host']))) { throw new \InvalidArgumentException('Invalid URI given'); } diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 5405874e..6c49c127 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -134,9 +134,6 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp /** @group internet */ public function testSuccessfulResponseEmitsEnd() { - // 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(new ClientConnectionManager(new Connector(), Loop::get())); $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); @@ -155,9 +152,6 @@ public function testSuccessfulResponseEmitsEnd() /** @group internet */ 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(new ClientConnectionManager(new Connector(), Loop::get())); $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 7b8ff84b..6a235703 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -389,10 +389,6 @@ public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() */ public function testCanAccessHttps() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - \React\Async\await($this->browser->get('https://www.google.com/')); } @@ -401,10 +397,6 @@ public function testCanAccessHttps() */ public function testVerifyPeerEnabledForBadSslRejects() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - $connector = new Connector(array( 'tls' => array( 'verify_peer' => true @@ -423,10 +415,6 @@ public function testVerifyPeerEnabledForBadSslRejects() */ public function testVerifyPeerDisabledForBadSslResolves() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - $connector = new Connector(array( 'tls' => array( 'verify_peer' => false diff --git a/tests/FunctionalHttpServerTest.php b/tests/FunctionalHttpServerTest.php index dcd79b3e..6b153b81 100644 --- a/tests/FunctionalHttpServerTest.php +++ b/tests/FunctionalHttpServerTest.php @@ -122,10 +122,6 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() public function testSecureHttpsOnRandomPort() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - $connector = new Connector(array( 'tls' => array('verify_peer' => false) )); @@ -155,10 +151,6 @@ public function testSecureHttpsOnRandomPort() public function testSecureHttpsReturnsData() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - $http = new HttpServer(function (RequestInterface $request) { return new Response( 200, @@ -193,10 +185,6 @@ public function testSecureHttpsReturnsData() public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - $connector = new Connector(array( 'tls' => array('verify_peer' => false) )); @@ -284,10 +272,6 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - try { $socket = new SocketServer('tls://127.0.0.1:443', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' @@ -322,10 +306,6 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - try { $socket = new SocketServer('tls://127.0.0.1:443', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' @@ -389,10 +369,6 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - try { $socket = new SocketServer('tls://127.0.0.1:80', array('tls' => array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' @@ -726,10 +702,6 @@ 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( diff --git a/tests/HttpServerTest.php b/tests/HttpServerTest.php index 72d48468..606c50a6 100644 --- a/tests/HttpServerTest.php +++ b/tests/HttpServerTest.php @@ -87,9 +87,6 @@ public function testSimpleRequestCallsRequestHandlerOnce() $this->assertSame(1, $called); } - /** - * @requires PHP 5.4 - */ public function testSimpleRequestCallsArrayRequestHandlerOnce() { $this->called = null; diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index 762d7bdb..e742ef6d 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -76,9 +76,6 @@ function (ServerRequestInterface $request) { $middleware($request); } - /** - * @requires PHP 7 - */ public function testThrowsIfHandlerThrowsThrowable() { $middleware = new MiddlewareRunner(array( diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index 87d6bf1b..1ed994b7 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -776,10 +776,6 @@ public function testServerParamsWillNotSetRemoteAddressForUnixDomainSockets() public function testServerParamsWontBeSetOnMissingUrls() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); - } - $request = null; $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index b4e3f2f8..152fece6 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -82,9 +82,6 @@ public function testRequestEventIsEmitted() $this->connection->emit('data', array($data)); } - /** - * @requires PHP 5.4 - */ public function testRequestEventIsEmittedForArrayCallable() { $this->called = null; @@ -2845,9 +2842,6 @@ function ($data) use (&$buffer) { $this->assertEquals('hello', $exception->getPrevious()->getMessage()); } - /** - * @requires PHP 7 - */ public function testResponseThrowableThrowInCallBackFunctionWillResultInErrorMessage() { $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { diff --git a/tests/Message/ResponseTest.php b/tests/Message/ResponseTest.php index a9a244c2..1c70ae3a 100644 --- a/tests/Message/ResponseTest.php +++ b/tests/Message/ResponseTest.php @@ -97,9 +97,6 @@ public function testHtmlMethodReturnsHtmlResponse() $this->assertEquals('Hello wörld!', (string) $response->getBody()); } - /** - * @requires PHP 5.4 - */ public function testJsonMethodReturnsPrettyPrintedJsonResponse() { $response = Response::json(array('text' => 'Hello wörld!')); @@ -109,9 +106,6 @@ public function testJsonMethodReturnsPrettyPrintedJsonResponse() $this->assertEquals("{\n \"text\": \"Hello wörld!\"\n}\n", (string) $response->getBody()); } - /** - * @requires PHP 5.6.6 - */ public function testJsonMethodReturnsZeroFractionsInJsonResponse() { $response = Response::json(1.0); @@ -132,11 +126,7 @@ public function testJsonMethodReturnsJsonTextForSimpleString() public function testJsonMethodThrowsForInvalidString() { - if (PHP_VERSION_ID < 50500) { - $this->setExpectedException('InvalidArgumentException', 'Unable to encode given data as JSON'); - } else { - $this->setExpectedException('InvalidArgumentException', 'Unable to encode given data as JSON: Malformed UTF-8 characters, possibly incorrectly encoded'); - } + $this->setExpectedException('InvalidArgumentException', 'Unable to encode given data as JSON: Malformed UTF-8 characters, possibly incorrectly encoded'); Response::json("Hello w\xF6rld!"); } diff --git a/tests/Message/UriTest.php b/tests/Message/UriTest.php index 05eec723..10f355df 100644 --- a/tests/Message/UriTest.php +++ b/tests/Message/UriTest.php @@ -130,11 +130,6 @@ public static function provideValidUris() */ 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); diff --git a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php index faf27cb6..23455e6c 100644 --- a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php +++ b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php @@ -120,9 +120,6 @@ public function testThrowsExceptionDirectlyFromMiddlewareWhenBelowLimit() }); } - /** - * @requires PHP 7 - */ public function testThrowsErrorDirectlyFromMiddlewareWhenBelowLimit() { $middleware = new LimitConcurrentRequestsMiddleware(1); diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index 40c23378..1c3b0b33 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -277,9 +277,6 @@ function (ServerRequestInterface $request) { $this->assertNull($exception->getPrevious()); } - /** - * @requires PHP 7 - */ public function testBufferingRejectsWhenNextHandlerThrowsErrorWhenStreamEnds() { $stream = new ThroughStream(); diff --git a/tests/Middleware/RequestBodyParserMiddlewareTest.php b/tests/Middleware/RequestBodyParserMiddlewareTest.php index 989abc81..b588bdd5 100644 --- a/tests/Middleware/RequestBodyParserMiddlewareTest.php +++ b/tests/Middleware/RequestBodyParserMiddlewareTest.php @@ -109,13 +109,6 @@ function (ServerRequestInterface $request) { public function testFormUrlencodedIgnoresBodyWithExcessiveNesting() { - // supported in all Zend PHP versions and HHVM - // ini setting does exist everywhere but HHVM: https://3v4l.org/hXLiK - // HHVM limits to 64 and returns an empty array structure: https://3v4l.org/j3DK2 - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM (limited to depth 64, but keeps empty array structure)'); - } - $allowed = (int)ini_get('max_input_nesting_level'); $middleware = new RequestBodyParserMiddleware(); @@ -144,12 +137,6 @@ function (ServerRequestInterface $request) { public function testFormUrlencodedTruncatesBodyWithExcessiveLength() { - // supported as of PHP 5.3.11, no HHVM support: https://3v4l.org/PiqnQ - // ini setting already exists in PHP 5.3.9: https://3v4l.org/VF6oV - if (defined('HHVM_VERSION') || PHP_VERSION_ID < 50311) { - $this->markTestSkipped('Not supported on HHVM and PHP < 5.3.11 (unlimited length)'); - } - $allowed = (int)ini_get('max_input_vars'); $middleware = new RequestBodyParserMiddleware(); @@ -243,13 +230,7 @@ function (ServerRequestInterface $request) { public function testMultipartFormDataIgnoresFieldWithExcessiveNesting() { - // supported in all Zend PHP versions and HHVM - // ini setting does exist everywhere but HHVM: https://3v4l.org/hXLiK - // HHVM limits to 64 and otherwise returns an empty array structure - $allowed = (int)ini_get('max_input_nesting_level'); - if ($allowed === 0) { - $allowed = 64; - } + $allowed = (int) ini_get('max_input_nesting_level'); $middleware = new RequestBodyParserMiddleware(); @@ -278,12 +259,7 @@ function (ServerRequestInterface $request) { public function testMultipartFormDataTruncatesBodyWithExcessiveLength() { - // ini setting exists in PHP 5.3.9, not in HHVM: https://3v4l.org/VF6oV - // otherwise default to 1000 as implemented within - $allowed = (int)ini_get('max_input_vars'); - if ($allowed === 0) { - $allowed = 1000; - } + $allowed = (int) ini_get('max_input_vars'); $middleware = new RequestBodyParserMiddleware(); @@ -319,12 +295,7 @@ function (ServerRequestInterface $request) { public function testMultipartFormDataTruncatesExcessiveNumberOfEmptyFileUploads() { - // ini setting exists in PHP 5.3.9, not in HHVM: https://3v4l.org/VF6oV - // otherwise default to 1000 as implemented within - $allowed = (int)ini_get('max_input_vars'); - if ($allowed === 0) { - $allowed = 1000; - } + $allowed = (int) ini_get('max_input_vars'); $middleware = new RequestBodyParserMiddleware(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 72b7be8d..88d8a3df 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -72,18 +72,12 @@ public function assertNotContainsString($needle, $haystack) public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null) { - if (method_exists($this, 'expectException')) { - // PHPUnit 5+ - $this->expectException($exception); - if ($exceptionMessage !== '') { - $this->expectExceptionMessage($exceptionMessage); - } - if ($exceptionCode !== null) { - $this->expectExceptionCode($exceptionCode); - } - } else { - // legacy PHPUnit 4 - parent::setExpectedException($exception, $exceptionMessage, $exceptionCode); + $this->expectException($exception); + if ($exceptionMessage !== '') { + $this->expectExceptionMessage($exceptionMessage); + } + if ($exceptionCode !== null) { + $this->expectExceptionCode($exceptionCode); } } From 4cbe56ef079c3bc90eaacc2cffefa46596a09d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 May 2024 16:37:32 +0200 Subject: [PATCH 147/152] Update PHP language syntax and remove legacy workarounds --- README.md | 131 ++-- examples/03-client-request-any.php | 4 +- examples/04-client-post-json.php | 12 +- examples/05-client-put-xml.php | 4 +- examples/11-client-http-proxy.php | 4 +- examples/12-client-socks-proxy.php | 4 +- examples/13-client-ssh-proxy.php | 4 +- .../21-client-request-streaming-to-stdout.php | 2 +- .../22-client-stream-upload-from-stdin.php | 4 +- examples/51-server-hello-world.php | 2 +- examples/52-server-count-visitors.php | 2 +- examples/53-server-whatsmyip.php | 2 +- examples/54-server-query-parameter.php | 2 +- examples/55-server-cookie-handling.php | 2 +- examples/56-server-sleep.php | 2 +- examples/57-server-error-handling.php | 2 +- examples/58-server-stream-response.php | 6 +- examples/59-server-json-api.php | 26 +- examples/61-server-hello-world-https.php | 12 +- examples/62-server-form-upload.php | 2 +- examples/63-server-streaming-request.php | 2 +- examples/71-server-http-proxy.php | 2 +- examples/72-server-http-connect-proxy.php | 12 +- examples/81-server-upgrade-echo.php | 10 +- examples/82-server-upgrade-chat.php | 10 +- examples/91-client-benchmark-download.php | 2 +- examples/92-client-benchmark-upload.php | 10 +- examples/99-server-benchmark-download.php | 12 +- src/Browser.php | 74 +- src/HttpServer.php | 21 +- src/Io/AbstractMessage.php | 18 +- src/Io/AbstractRequest.php | 4 +- src/Io/BufferedBody.php | 2 +- src/Io/ChunkedDecoder.php | 14 +- src/Io/ChunkedEncoder.php | 18 +- src/Io/ClientConnectionManager.php | 27 +- src/Io/ClientRequestStream.php | 59 +- src/Io/Clock.php | 7 +- src/Io/CloseProtectionStream.php | 22 +- src/Io/EmptyBodyStream.php | 4 +- src/Io/HttpBodyStream.php | 14 +- src/Io/LengthLimitedStream.php | 16 +- src/Io/MiddlewareRunner.php | 8 +- src/Io/MultipartParser.php | 8 +- src/Io/PauseBufferStream.php | 18 +- src/Io/ReadableBodyStream.php | 30 +- src/Io/RequestHeaderParser.php | 35 +- src/Io/Sender.php | 8 +- src/Io/StreamingServer.php | 66 +- src/Io/Transaction.php | 50 +- src/Io/UploadedFile.php | 4 +- src/Message/Request.php | 2 +- src/Message/Response.php | 41 +- src/Message/ServerRequest.php | 20 +- src/Message/Uri.php | 8 +- .../LimitConcurrentRequestsMiddleware.php | 57 +- .../RequestBodyBufferMiddleware.php | 5 +- .../RequestBodyParserMiddleware.php | 4 +- tests/BrowserTest.php | 223 +++--- tests/Client/FunctionalIntegrationTest.php | 41 +- tests/FunctionalBrowserTest.php | 182 ++--- tests/FunctionalHttpServerTest.php | 205 +++--- tests/HttpServerTest.php | 78 +-- tests/Io/AbstractMessageTest.php | 100 +-- tests/Io/AbstractRequestTest.php | 96 +-- tests/Io/BufferedBodyTest.php | 2 +- tests/Io/ChunkedDecoderTest.php | 176 ++--- tests/Io/ChunkedEncoderTest.php | 8 +- tests/Io/ClientConnectionManagerTest.php | 43 +- tests/Io/ClientRequestStreamTest.php | 338 +++++---- tests/Io/CloseProtectionStreamTest.php | 8 +- tests/Io/EmptyBodyStreamTest.php | 4 +- tests/Io/HttpBodyStreamTest.php | 14 +- tests/Io/IniUtilTest.php | 52 +- tests/Io/LengthLimitedStreamTest.php | 14 +- tests/Io/MiddlewareRunnerTest.php | 170 ++--- tests/Io/MultipartParserTest.php | 232 +++--- tests/Io/PauseBufferStreamTest.php | 8 +- tests/Io/ReadableBodyStreamTest.php | 2 +- tests/Io/RequestHeaderParserTest.php | 141 ++-- tests/Io/SenderTest.php | 44 +- tests/Io/StreamingServerTest.php | 659 +++++++++--------- tests/Io/TransactionTest.php | 326 +++++---- tests/Io/UploadedFileTest.php | 12 +- tests/Message/RequestTest.php | 8 +- tests/Message/ResponseTest.php | 26 +- tests/Message/ServerRequestTest.php | 132 ++-- tests/Message/UriTest.php | 240 +++---- .../LimitConcurrentRequestsMiddlewareTest.php | 38 +- tests/Middleware/ProcessStack.php | 4 +- .../RequestBodyBufferMiddlewareTest.php | 41 +- .../RequestBodyParserMiddlewareTest.php | 94 +-- tests/TestCase.php | 10 +- tests/benchmark-middleware-runner.php | 2 +- 94 files changed, 2329 insertions(+), 2397 deletions(-) diff --git a/README.md b/README.md index 9cdf7c09..7761a245 100644 --- a/README.md +++ b/README.md @@ -144,12 +144,12 @@ Most importantly, this project provides a [`Browser`](#browser) object that offers several methods that resemble the HTTP protocol methods: ```php -$browser->get($url, array $headers = array()); -$browser->head($url, array $headers = array()); -$browser->post($url, array $headers = array(), string|ReadableStreamInterface $body = ''); -$browser->delete($url, array $headers = array(), string|ReadableStreamInterface $body = ''); -$browser->put($url, array $headers = array(), string|ReadableStreamInterface $body = ''); -$browser->patch($url, array $headers = array(), string|ReadableStreamInterface $body = ''); +$browser->get($url, array $headers = []); +$browser->head($url, array $headers = []); +$browser->post($url, array $headers = [], string|ReadableStreamInterface $body = ''); +$browser->delete($url, array $headers = [], string|ReadableStreamInterface $body = ''); +$browser->put($url, array $headers = [], string|ReadableStreamInterface $body = ''); +$browser->patch($url, array $headers = [], string|ReadableStreamInterface $body = ''); ``` Each of these methods requires a `$url` and some optional parameters to send an @@ -285,9 +285,9 @@ like this: ```php $browser = new React\Http\Browser( new React\Socket\Connector( - array( + [ 'timeout' => 5 - ) + ] ) ); ``` @@ -323,9 +323,9 @@ $token = 'abc123'; $promise = $browser->get( 'https://example.com/api', - array( + [ 'Authorization' => 'Bearer ' . $token - ) + ] ); ``` @@ -411,10 +411,10 @@ Similarly, you can also process multiple requests concurrently and await an arra use function React\Async\await; use function React\Promise\all; -$promises = array( +$promises = [ $browser->get('http://example.com/'), $browser->get('http://www.example.org/'), -); +]; $responses = await(all($promises)); ``` @@ -540,7 +540,7 @@ You can invoke the following methods on the message body: $body->on($event, $callback); $body->eof(); $body->isReadable(); -$body->pipe(React\Stream\WritableStreamInterface $dest, array $options = array()); +$body->pipe(React\Stream\WritableStreamInterface $dest, array $options = []); $body->close(); $body->pause(); $body->resume(); @@ -575,10 +575,10 @@ Consider looking into also using [react/promise-stream](https://github.com/react The resulting streaming code could look something like this: ```php -use React\Promise\Stream; +use function React\Promise\Stream\unwrapReadable; function download(Browser $browser, string $url): React\Stream\ReadableStreamInterface { - return Stream\unwrapReadable( + return unwrapReadable( $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { return $response->getBody(); }) @@ -606,7 +606,7 @@ implementing [ReactPHP's `ReadableStreamInterface`](https://github.com/reactphp/ to the [request methods](#request-methods) like this: ```php -$browser->post($url, array(), $stream)->then(function (Psr\Http\Message\ResponseInterface $response) { +$browser->post($url, [], $stream)->then(function (Psr\Http\Message\ResponseInterface $response) { echo 'Successfully sent.'; }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; @@ -623,7 +623,7 @@ Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); -$browser->post($url, array('Content-Length' => '11'), $body); +$browser->post($url, ['Content-Length' => '11'], $body); ``` If the streaming request body emits an `error` event or is explicitly closed @@ -645,10 +645,10 @@ protocol, such as plain HTTP and TLS-encrypted HTTPS. ```php $proxy = new Clue\React\HttpProxy\ProxyConnector('127.0.0.1:8080'); -$connector = new React\Socket\Connector(array( +$connector = new React\Socket\Connector([ 'tcp' => $proxy, 'dns' => false -)); +]); $browser = new React\Http\Browser($connector); ``` @@ -669,10 +669,10 @@ only, this can technically be used to tunnel any TCP/IP-based protocol. ```php $proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); -$connector = new React\Socket\Connector(array( +$connector = new React\Socket\Connector([ 'tcp' => $proxy, 'dns' => false -)); +]); $browser = new React\Http\Browser($connector); ``` @@ -698,10 +698,10 @@ plain HTTP and TLS-encrypted HTTPS. ```php $proxy = new Clue\React\SshProxy\SshSocksConnector('alice@example.com'); -$connector = new React\Socket\Connector(array( +$connector = new React\Socket\Connector([ 'tcp' => $proxy, 'dns' => false -)); +]); $browser = new React\Http\Browser($connector); ``` @@ -931,11 +931,11 @@ using a secure TLS listen address, a certificate file and optional ```php $http = new React\Http\HttpServer($handler); -$socket = new React\Socket\SocketServer('tls://0.0.0.0:8443', array( - 'tls' => array( +$socket = new React\Socket\SocketServer('tls://0.0.0.0:8443', [ + 'tls' => [ 'local_cert' => __DIR__ . '/localhost.pem' - ) -)); + ] +]); $http->listen($socket); ``` @@ -1456,9 +1456,9 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf return new React\Http\Message\Response( React\Http\Message\Response::STATUS_OK, - array( + [ 'Content-Type' => 'text/plain' - ), + ], $stream ); }); @@ -1558,10 +1558,10 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterf return new React\Http\Message\Response( React\Http\Message\Response::STATUS_OK, - array( + [ 'Content-Length' => '13', 'Content-Type' => 'text/plain', - ), + ], $stream ); }); @@ -1628,9 +1628,9 @@ a custom `Server` response header like this: $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( React\Http\Message\Response::STATUS_OK, - array( + [ 'Server' => 'PHP/3' - ) + ] ); }); ``` @@ -1643,9 +1643,9 @@ string value like this: $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( React\Http\Message\Response::STATUS_OK, - array( + [ 'Server' => '' - ) + ] ); }); ``` @@ -1658,9 +1658,9 @@ like this: $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( React\Http\Message\Response::STATUS_OK, - array( + [ 'Date' => gmdate('D, d M Y H:i:s \G\M\T') - ) + ] ); }); ``` @@ -1673,9 +1673,9 @@ like this: $http = new React\Http\HttpServer(function (ServerRequestInterface $request) { return new React\Http\Message\Response( React\Http\Message\Response::STATUS_OK, - array( + [ 'Date' => '' - ) + ] ); }); ``` @@ -1871,16 +1871,16 @@ 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(array( +$connector = new React\Socket\Connector([ 'dns' => '127.0.0.1', - 'tcp' => array( + 'tcp' => [ 'bindto' => '192.168.10.1:0' - ), - 'tls' => array( + ], + 'tls' => [ 'verify_peer' => false, 'verify_peer_name' => false - ) -)); + ] +]); $browser = new React\Http\Browser($connector); ``` @@ -1895,7 +1895,7 @@ given event loop instance. #### get() -The `get(string $url, array $headers = array()): PromiseInterface` method can be used to +The `get(string $url, array $headers = []): PromiseInterface` method can be used to send an HTTP GET request. ```php @@ -1910,7 +1910,7 @@ See also [GET request client example](examples/01-client-get-request.php). #### post() -The `post(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +The `post(string $url, array $headers = [], string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an HTTP POST request. ```php @@ -1958,12 +1958,12 @@ Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); -$browser->post($url, array('Content-Length' => '11'), $body); +$browser->post($url, ['Content-Length' => '11'), $body); ``` #### head() -The `head(string $url, array $headers = array()): PromiseInterface` method can be used to +The `head(string $url, array $headers = []): PromiseInterface` method can be used to send an HTTP HEAD request. ```php @@ -1976,7 +1976,7 @@ $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $respons #### patch() -The `patch(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +The `patch(string $url, array $headers = [], string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an HTTP PATCH request. ```php @@ -2005,12 +2005,12 @@ Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); -$browser->patch($url, array('Content-Length' => '11'), $body); +$browser->patch($url, ['Content-Length' => '11'], $body); ``` #### put() -The `put(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +The `put(string $url, array $headers = [], string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an HTTP PUT request. ```php @@ -2041,12 +2041,12 @@ Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); -$browser->put($url, array('Content-Length' => '11'), $body); +$browser->put($url, ['Content-Length' => '11'], $body); ``` #### delete() -The `delete(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +The `delete(string $url, array $headers = [], string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an HTTP DELETE request. ```php @@ -2059,7 +2059,7 @@ $browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $respo #### request() -The `request(string $method, string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +The `request(string $method, string $url, array $headers = [], string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an arbitrary HTTP request. The preferred way to send an HTTP request is by using the above @@ -2093,12 +2093,12 @@ Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); -$browser->request('POST', $url, array('Content-Length' => '11'), $body); +$browser->request('POST', $url, ['Content-Length' => '11'], $body); ``` #### requestStreaming() -The `requestStreaming(string $method, string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +The `requestStreaming(string $method, string $url, array $headers = [], string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to send an arbitrary HTTP request and receive a streaming response without buffering the response body. The preferred way to send an HTTP request is by using the above @@ -2157,7 +2157,7 @@ Loop::addTimer(1.0, function () use ($body) { $body->end("hello world"); }); -$browser->requestStreaming('POST', $url, array('Content-Length' => '11'), $body); +$browser->requestStreaming('POST', $url, ['Content-Length' => '11'], $body); ``` #### withTimeout() @@ -2428,9 +2428,9 @@ represent an outgoing server response message. ```php $response = new React\Http\Message\Response( React\Http\Message\Response::STATUS_OK, - array( + [ 'Content-Type' => 'text/html' - ), + ], "Hello world!\n" ); ``` @@ -2528,10 +2528,9 @@ values in the data must be encoded in UTF-8 (Unicode). If the encoding fails, this method will throw an `InvalidArgumentException`. By default, the given structured data will be encoded with the flags as -shown above. This includes pretty printing (PHP 5.4+) and preserving -zero fractions for `float` values (PHP 5.6.6+) to ease debugging. It is -assumed any additional data overhead is usually compensated by using HTTP -response compression. +shown above. This includes pretty printing and preserving zero fractions +for `float` values to ease debugging. It is assumed any additional data +overhead is usually compensated by using HTTP response compression. If you want to use a different status code or custom HTTP response headers, you can manipulate the returned response object using the @@ -2900,9 +2899,9 @@ $handler = function (Psr\Http\Message\ServerRequestInterface $request) { return new React\Http\Message\Response( React\Http\Message\Response::STATUS_OK, - array( + [ 'Content-Type' => 'text/plain' - ), + ], $name . ' uploaded ' . $uploaded ); }; diff --git a/examples/03-client-request-any.php b/examples/03-client-request-any.php index d7558bd6..9ee80131 100644 --- a/examples/03-client-request-any.php +++ b/examples/03-client-request-any.php @@ -10,13 +10,13 @@ $client = new Browser(); -$promises = array( +$promises = [ $client->head('http://www.github.com/clue/http-react'), $client->get('https://httpbingo.org/'), $client->get('https://google.com/'), $client->get('http://www.lueck.tv/psocksd'), $client->get('http://httpbingo.org/absolute-redirect/5') -); +]; React\Promise\any($promises)->then(function (ResponseInterface $response) use ($promises) { // first response arrived => cancel all other pending requests diff --git a/examples/04-client-post-json.php b/examples/04-client-post-json.php index 18fa596d..2ecc0636 100644 --- a/examples/04-client-post-json.php +++ b/examples/04-client-post-json.php @@ -7,19 +7,19 @@ $client = new Browser(); -$data = array( - 'name' => array( +$data = [ + 'name' => [ 'first' => 'Alice', 'name' => 'Smith' - ), + ], 'email' => 'alice@example.com' -); +]; $client->post( 'https://httpbingo.org/post', - array( + [ 'Content-Type' => 'application/json' - ), + ], json_encode($data) )->then(function (ResponseInterface $response) { echo (string) $response->getBody(); diff --git a/examples/05-client-put-xml.php b/examples/05-client-put-xml.php index 10ee46fc..af01c47a 100644 --- a/examples/05-client-put-xml.php +++ b/examples/05-client-put-xml.php @@ -14,9 +14,9 @@ $client->put( 'https://httpbingo.org/put', - array( + [ 'Content-Type' => 'text/xml' - ), + ], $xml->asXML() )->then(function (ResponseInterface $response) { echo (string) $response->getBody(); diff --git a/examples/11-client-http-proxy.php b/examples/11-client-http-proxy.php index ec7fc2b6..f15cf2a0 100644 --- a/examples/11-client-http-proxy.php +++ b/examples/11-client-http-proxy.php @@ -16,10 +16,10 @@ $proxy = new Clue\React\HttpProxy\ProxyConnector(getenv('http_proxy') ?: '127.0.0.1:8080'); // create a Browser object that uses the HTTP CONNECT proxy client for connections -$connector = new Connector(array( +$connector = new Connector([ 'tcp' => $proxy, 'dns' => false -)); +]); $browser = new Browser($connector); diff --git a/examples/12-client-socks-proxy.php b/examples/12-client-socks-proxy.php index 8c525509..0e0039ca 100644 --- a/examples/12-client-socks-proxy.php +++ b/examples/12-client-socks-proxy.php @@ -16,10 +16,10 @@ $proxy = new Clue\React\Socks\Client(getenv('socks_proxy') ?: '127.0.0.1:1080'); // create a Browser object that uses the SOCKS proxy client for connections -$connector = new Connector(array( +$connector = new Connector([ 'tcp' => $proxy, 'dns' => false -)); +]); $browser = new Browser($connector); diff --git a/examples/13-client-ssh-proxy.php b/examples/13-client-ssh-proxy.php index 93e6e256..e387d4fc 100644 --- a/examples/13-client-ssh-proxy.php +++ b/examples/13-client-ssh-proxy.php @@ -12,10 +12,10 @@ $proxy = new Clue\React\SshProxy\SshSocksConnector(getenv('ssh_proxy') ?: 'alice@localhost'); // create a Browser object that uses the SSH proxy client for connections -$connector = new Connector(array( +$connector = new Connector([ 'tcp' => $proxy, 'dns' => false -)); +]); $browser = new Browser($connector); diff --git a/examples/21-client-request-streaming-to-stdout.php b/examples/21-client-request-streaming-to-stdout.php index b3cbbe39..47c2371f 100644 --- a/examples/21-client-request-streaming-to-stdout.php +++ b/examples/21-client-request-streaming-to-stdout.php @@ -17,7 +17,7 @@ $out = new WritableResourceStream(STDOUT); $info = new WritableResourceStream(STDERR); -$url = isset($argv[1]) ? $argv[1] : 'http://google.com/'; +$url = $argv[1] ?? 'http://google.com/'; $info->write('Requesting ' . $url . '…' . PHP_EOL); $client->requestStreaming('GET', $url)->then(function (ResponseInterface $response) use ($info, $out) { diff --git a/examples/22-client-stream-upload-from-stdin.php b/examples/22-client-stream-upload-from-stdin.php index f0a68c5f..438b6280 100644 --- a/examples/22-client-stream-upload-from-stdin.php +++ b/examples/22-client-stream-upload-from-stdin.php @@ -15,10 +15,10 @@ $in = new ReadableResourceStream(STDIN); -$url = isset($argv[1]) ? $argv[1] : 'https://httpbingo.org/post'; +$url = $argv[1] ?? 'https://httpbingo.org/post'; echo 'Sending STDIN as POST to ' . $url . '…' . PHP_EOL; -$client->post($url, array('Content-Type' => 'text/plain'), $in)->then(function (ResponseInterface $response) { +$client->post($url, ['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/51-server-hello-world.php b/examples/51-server-hello-world.php index 9ff84eee..e25efc65 100644 --- a/examples/51-server-hello-world.php +++ b/examples/51-server-hello-world.php @@ -8,7 +8,7 @@ ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/52-server-count-visitors.php b/examples/52-server-count-visitors.php index 341f9498..333a8011 100644 --- a/examples/52-server-count-visitors.php +++ b/examples/52-server-count-visitors.php @@ -9,7 +9,7 @@ ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/53-server-whatsmyip.php b/examples/53-server-whatsmyip.php index 1e394b9e..d9018a64 100644 --- a/examples/53-server-whatsmyip.php +++ b/examples/53-server-whatsmyip.php @@ -10,7 +10,7 @@ ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/54-server-query-parameter.php b/examples/54-server-query-parameter.php index 9b2d5749..507e8862 100644 --- a/examples/54-server-query-parameter.php +++ b/examples/54-server-query-parameter.php @@ -17,7 +17,7 @@ ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/55-server-cookie-handling.php b/examples/55-server-cookie-handling.php index b5e68862..b2b62100 100644 --- a/examples/55-server-cookie-handling.php +++ b/examples/55-server-cookie-handling.php @@ -18,7 +18,7 @@ )->withHeader('Set-Cookie', $key . '=' . urlencode('Hello world!')); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/56-server-sleep.php b/examples/56-server-sleep.php index 2a3c9027..45f64149 100644 --- a/examples/56-server-sleep.php +++ b/examples/56-server-sleep.php @@ -18,7 +18,7 @@ }); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/57-server-error-handling.php b/examples/57-server-error-handling.php index a9fb6bad..c5141161 100644 --- a/examples/57-server-error-handling.php +++ b/examples/57-server-error-handling.php @@ -15,7 +15,7 @@ ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/58-server-stream-response.php b/examples/58-server-stream-response.php index 9d12461a..d99b1548 100644 --- a/examples/58-server-stream-response.php +++ b/examples/58-server-stream-response.php @@ -32,14 +32,14 @@ return new Response( Response::STATUS_OK, - array( + [ 'Content-Type' => 'text/plain' - ), + ], $stream ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/59-server-json-api.php b/examples/59-server-json-api.php index f48be7e3..7e7477c0 100644 --- a/examples/59-server-json-api.php +++ b/examples/59-server-json-api.php @@ -12,30 +12,30 @@ $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { if ($request->getHeaderLine('Content-Type') !== 'application/json') { - return Response::json( - array('error' => 'Only supports application/json') - )->withStatus(Response::STATUS_UNSUPPORTED_MEDIA_TYPE); + return Response::json([ + 'error' => 'Only supports application/json' + ])->withStatus(Response::STATUS_UNSUPPORTED_MEDIA_TYPE); } $input = json_decode($request->getBody()->getContents()); if (json_last_error() !== JSON_ERROR_NONE) { - return Response::json( - array('error' => 'Invalid JSON data given') - )->withStatus(Response::STATUS_BAD_REQUEST); + return Response::json([ + 'error' => 'Invalid JSON data given' + ])->withStatus(Response::STATUS_BAD_REQUEST); } if (!isset($input->name) || !is_string($input->name)) { - return Response::json( - array('error' => 'JSON data does not contain a string "name" property') - )->withStatus(Response::STATUS_UNPROCESSABLE_ENTITY); + return Response::json([ + 'error' => 'JSON data does not contain a string "name" property' + ])->withStatus(Response::STATUS_UNPROCESSABLE_ENTITY); } - return Response::json( - array('message' => 'Hello ' . $input->name) - ); + return Response::json([ + 'message' => 'Hello ' . $input->name + ]); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/61-server-hello-world-https.php b/examples/61-server-hello-world-https.php index 23906430..8ce487c7 100644 --- a/examples/61-server-hello-world-https.php +++ b/examples/61-server-hello-world-https.php @@ -8,12 +8,12 @@ ); }); -$uri = 'tls://' . (isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); -$socket = new React\Socket\SocketServer($uri, array( - 'tls' => array( - 'local_cert' => isset($argv[2]) ? $argv[2] : __DIR__ . '/localhost.pem' - ) -)); +$uri = 'tls://' . ($argv[1] ?? '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($uri, [ + 'tls' => [ + 'local_cert' => $argv[2] ?? __DIR__ . '/localhost.pem' + ] +]); $http->listen($socket); $socket->on('error', function (Exception $e) { diff --git a/examples/62-server-form-upload.php b/examples/62-server-form-upload.php index 52864c82..9c5c8aa3 100644 --- a/examples/62-server-form-upload.php +++ b/examples/62-server-form-upload.php @@ -124,7 +124,7 @@ $handler ); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/63-server-streaming-request.php b/examples/63-server-streaming-request.php index fef6f008..8ed3a4e1 100644 --- a/examples/63-server-streaming-request.php +++ b/examples/63-server-streaming-request.php @@ -38,7 +38,7 @@ function (Psr\Http\Message\ServerRequestInterface $request) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/71-server-http-proxy.php b/examples/71-server-http-proxy.php index de9fa10b..d513ede2 100644 --- a/examples/71-server-http-proxy.php +++ b/examples/71-server-http-proxy.php @@ -32,7 +32,7 @@ ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/72-server-http-connect-proxy.php b/examples/72-server-http-connect-proxy.php index 0500822a..98a21f34 100644 --- a/examples/72-server-http-connect-proxy.php +++ b/examples/72-server-http-connect-proxy.php @@ -20,10 +20,10 @@ if ($request->getMethod() !== 'CONNECT') { return new Response( Response::STATUS_METHOD_NOT_ALLOWED, - array( + [ 'Content-Type' => 'text/plain', 'Allow' => 'CONNECT' - ), + ], 'This is an HTTP CONNECT (secure HTTPS) proxy' ); } @@ -34,23 +34,23 @@ function (ConnectionInterface $remote) { // connection established => forward data return new Response( Response::STATUS_OK, - array(), + [], $remote ); }, function (Exception $e) { return new Response( Response::STATUS_BAD_GATEWAY, - array( + [ 'Content-Type' => 'text/plain' - ), + ], 'Unable to connect: ' . $e->getMessage() ); } ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/81-server-upgrade-echo.php b/examples/81-server-upgrade-echo.php index cd3dc156..fbc4d75a 100644 --- a/examples/81-server-upgrade-echo.php +++ b/examples/81-server-upgrade-echo.php @@ -31,9 +31,9 @@ if ($request->getHeaderLine('Upgrade') !== 'echo' || $request->getProtocolVersion() === '1.0') { return new Response( Response::STATUS_UPGRADE_REQUIRED, - array( + [ 'Upgrade' => 'echo' - ), + ], '"Upgrade: echo" required' ); } @@ -49,14 +49,14 @@ return new Response( Response::STATUS_SWITCHING_PROTOCOLS, - array( + [ 'Upgrade' => 'echo' - ), + ], $stream ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/82-server-upgrade-chat.php b/examples/82-server-upgrade-chat.php index bd791fb0..00788922 100644 --- a/examples/82-server-upgrade-chat.php +++ b/examples/82-server-upgrade-chat.php @@ -39,9 +39,9 @@ if ($request->getHeaderLine('Upgrade') !== 'chat' || $request->getProtocolVersion() === '1.0') { return new Response( Response::STATUS_UPGRADE_REQUIRED, - array( + [ 'Upgrade' => 'chat' - ), + ], '"Upgrade: chat" required' ); } @@ -77,14 +77,14 @@ return new Response( Response::STATUS_SWITCHING_PROTOCOLS, - array( + [ 'Upgrade' => 'chat' - ), + ], $stream ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/examples/91-client-benchmark-download.php b/examples/91-client-benchmark-download.php index 712d9f10..f74b8925 100644 --- a/examples/91-client-benchmark-download.php +++ b/examples/91-client-benchmark-download.php @@ -16,7 +16,7 @@ use React\Http\Browser; use React\Stream\ReadableStreamInterface; -$url = isset($argv[1]) ? $argv[1] : 'http://google.com/'; +$url = $argv[1] ?? 'http://google.com/'; require __DIR__ . '/../vendor/autoload.php'; diff --git a/examples/92-client-benchmark-upload.php b/examples/92-client-benchmark-upload.php index 9fa1848a..10434bfd 100644 --- a/examples/92-client-benchmark-upload.php +++ b/examples/92-client-benchmark-upload.php @@ -55,7 +55,7 @@ public function resume() $this->paused = false; while ($this->position < $this->count && !$this->paused) { ++$this->position; - $this->emit('data', array($this->chunk)); + $this->emit('data', [$this->chunk]); } // end once the last chunk has been written @@ -65,7 +65,7 @@ public function resume() } } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { return Util::pipe($this, $dest, $options); } @@ -95,8 +95,8 @@ public function getPosition() $client = new Browser(); -$url = isset($argv[1]) ? $argv[1] : 'http://httpbin.org/post'; -$n = isset($argv[2]) ? $argv[2] : 10; +$url = $argv[1] ?? 'http://httpbin.org/post'; +$n = $argv[2] ?? 10; $source = new ChunkRepeater(str_repeat('x', 1000000), $n); Loop::futureTick(function () use ($source) { $source->resume(); @@ -109,7 +109,7 @@ public function getPosition() printf("\r%d bytes in %0.3fs...", $source->getPosition(), microtime(true) - $start); }); -$client->post($url, array('Content-Length' => $n * 1000000), $source)->then(function (ResponseInterface $response) use ($source, $report, $start) { +$client->post($url, ['Content-Length' => $n * 1000000], $source)->then(function (ResponseInterface $response) use ($source, $report, $start) { $now = microtime(true); Loop::cancelTimer($report); diff --git a/examples/99-server-benchmark-download.php b/examples/99-server-benchmark-download.php index ddd4760a..ee1cfc8f 100644 --- a/examples/99-server-benchmark-download.php +++ b/examples/99-server-benchmark-download.php @@ -52,7 +52,7 @@ public function resume() $this->paused = false; while ($this->position < $this->count && !$this->paused) { ++$this->position; - $this->emit('data', array($this->chunk)); + $this->emit('data', [$this->chunk]); } // end once the last chunk has been written @@ -62,7 +62,7 @@ public function resume() } } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { return; } @@ -106,19 +106,19 @@ public function getSize() return new Response(Response::STATUS_NOT_FOUND); } - React\EventLoop\Loop::addTimer(0, array($stream, 'resume')); + React\EventLoop\Loop::addTimer(0, [$stream, 'resume']); return new Response( Response::STATUS_OK, - array( + [ 'Content-Type' => 'application/octet-data', 'Content-Length' => $stream->getSize() - ), + ], $stream ); }); -$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '0.0.0.0:0'); +$socket = new React\Socket\SocketServer($argv[1] ?? '0.0.0.0:0'); $http->listen($socket); echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; diff --git a/src/Browser.php b/src/Browser.php index a24d24e7..06e194d9 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -22,9 +22,9 @@ class Browser private $transaction; private $baseUrl; private $protocolVersion = '1.1'; - private $defaultHeaders = array( + private $defaultHeaders = [ 'User-Agent' => 'ReactPHP/1' - ); + ]; /** * The `Browser` is responsible for sending HTTP requests to your HTTP server @@ -45,16 +45,16 @@ class Browser * [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): * * ```php - * $connector = new React\Socket\Connector(array( + * $connector = new React\Socket\Connector([ * 'dns' => '127.0.0.1', - * 'tcp' => array( + * 'tcp' => [ * 'bindto' => '192.168.10.1:0' - * ), - * 'tls' => array( + * ], + * 'tls' => [ * 'verify_peer' => false, * 'verify_peer_name' => false - * ) - * )); + * ] + * ]); * * $browser = new React\Http\Browser($connector); * ``` @@ -70,7 +70,7 @@ class Browser */ public function __construct(ConnectorInterface $connector = null, LoopInterface $loop = null) { - $loop = $loop ?: Loop::get(); + $loop = $loop ?? Loop::get(); $this->transaction = new Transaction( Sender::createFromLoop($loop, $connector), $loop @@ -94,7 +94,7 @@ public function __construct(ConnectorInterface $connector = null, LoopInterface * @param array $headers * @return PromiseInterface */ - public function get($url, array $headers = array()) + public function get($url, array $headers = []) { return $this->requestMayBeStreaming('GET', $url, $headers); } @@ -147,7 +147,7 @@ public function get($url, array $headers = array()) * $body->end("hello world"); * }); * - * $browser->post($url, array('Content-Length' => '11'), $body); + * $browser->post($url, ['Content-Length' => '11'], $body); * ``` * * @param string $url URL for the request. @@ -155,7 +155,7 @@ public function get($url, array $headers = array()) * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - public function post($url, array $headers = array(), $body = '') + public function post($url, array $headers = [], $body = '') { return $this->requestMayBeStreaming('POST', $url, $headers, $body); } @@ -175,7 +175,7 @@ public function post($url, array $headers = array(), $body = '') * @param array $headers * @return PromiseInterface */ - public function head($url, array $headers = array()) + public function head($url, array $headers = []) { return $this->requestMayBeStreaming('HEAD', $url, $headers); } @@ -209,7 +209,7 @@ public function head($url, array $headers = array()) * $body->end("hello world"); * }); * - * $browser->patch($url, array('Content-Length' => '11'), $body); + * $browser->patch($url, ['Content-Length' => '11'], $body); * ``` * * @param string $url URL for the request. @@ -217,7 +217,7 @@ public function head($url, array $headers = array()) * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - public function patch($url, array $headers = array(), $body = '') + public function patch($url, array $headers = [], $body = '') { return $this->requestMayBeStreaming('PATCH', $url , $headers, $body); } @@ -253,7 +253,7 @@ public function patch($url, array $headers = array(), $body = '') * $body->end("hello world"); * }); * - * $browser->put($url, array('Content-Length' => '11'), $body); + * $browser->put($url, ['Content-Length' => '11'], $body); * ``` * * @param string $url URL for the request. @@ -261,7 +261,7 @@ public function patch($url, array $headers = array(), $body = '') * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - public function put($url, array $headers = array(), $body = '') + public function put($url, array $headers = [], $body = '') { return $this->requestMayBeStreaming('PUT', $url, $headers, $body); } @@ -282,7 +282,7 @@ public function put($url, array $headers = array(), $body = '') * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - public function delete($url, array $headers = array(), $body = '') + public function delete($url, array $headers = [], $body = '') { return $this->requestMayBeStreaming('DELETE', $url, $headers, $body); } @@ -321,7 +321,7 @@ public function delete($url, array $headers = array(), $body = '') * $body->end("hello world"); * }); * - * $browser->request('POST', $url, array('Content-Length' => '11'), $body); + * $browser->request('POST', $url, ['Content-Length' => '11'], $body); * ``` * * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. @@ -330,9 +330,9 @@ public function delete($url, array $headers = array(), $body = '') * @param string|ReadableStreamInterface $body HTTP request body contents * @return PromiseInterface */ - public function request($method, $url, array $headers = array(), $body = '') + public function request($method, $url, array $headers = [], $body = '') { - return $this->withOptions(array('streaming' => false))->requestMayBeStreaming($method, $url, $headers, $body); + return $this->withOptions(['streaming' => false])->requestMayBeStreaming($method, $url, $headers, $body); } /** @@ -394,7 +394,7 @@ public function request($method, $url, array $headers = array(), $body = '') * $body->end("hello world"); * }); * - * $browser->requestStreaming('POST', $url, array('Content-Length' => '11'), $body); + * $browser->requestStreaming('POST', $url, ['Content-Length' => '11'], $body); * ``` * * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. @@ -403,9 +403,9 @@ public function request($method, $url, array $headers = array(), $body = '') * @param string|ReadableStreamInterface $body HTTP request body contents * @return PromiseInterface */ - public function requestStreaming($method, $url, $headers = array(), $body = '') + public function requestStreaming($method, $url, $headers = [], $body = '') { - return $this->withOptions(array('streaming' => true))->requestMayBeStreaming($method, $url, $headers, $body); + return $this->withOptions(['streaming' => true])->requestMayBeStreaming($method, $url, $headers, $body); } /** @@ -450,9 +450,9 @@ public function withTimeout($timeout) $timeout = 0; } - return $this->withOptions(array( + return $this->withOptions([ 'timeout' => $timeout, - )); + ]); } /** @@ -512,10 +512,10 @@ public function withTimeout($timeout) */ public function withFollowRedirects($followRedirects) { - return $this->withOptions(array( + return $this->withOptions([ 'followRedirects' => $followRedirects !== false, 'maxRedirects' => \is_bool($followRedirects) ? null : $followRedirects - )); + ]); } /** @@ -566,9 +566,9 @@ public function withFollowRedirects($followRedirects) */ public function withRejectErrorResponse($obeySuccessCode) { - return $this->withOptions(array( + return $this->withOptions([ 'obeySuccessCode' => $obeySuccessCode, - )); + ]); } /** @@ -618,7 +618,7 @@ public function withBase($baseUrl) } $browser->baseUrl = new Uri($baseUrl); - if (!\in_array($browser->baseUrl->getScheme(), array('http', 'https')) || $browser->baseUrl->getHost() === '') { + if (!\in_array($browser->baseUrl->getScheme(), ['http', 'https']) || $browser->baseUrl->getHost() === '') { throw new \InvalidArgumentException('Base URL must be absolute'); } @@ -653,7 +653,7 @@ public function withBase($baseUrl) */ public function withProtocolVersion($protocolVersion) { - if (!\in_array($protocolVersion, array('1.0', '1.1'), true)) { + if (!\in_array($protocolVersion, ['1.0', '1.1'], true)) { throw new InvalidArgumentException('Invalid HTTP protocol version, must be one of "1.1" or "1.0"'); } @@ -706,9 +706,9 @@ public function withProtocolVersion($protocolVersion) */ public function withResponseBuffer($maximumSize) { - return $this->withOptions(array( + return $this->withOptions([ 'maximumSize' => $maximumSize - )); + ]); } /** @@ -777,13 +777,13 @@ public function withoutHeader($header) * * ```php * // deprecated - * $newBrowser = $browser->withOptions(array( + * $newBrowser = $browser->withOptions([ * 'timeout' => null, // see withTimeout() instead * 'followRedirects' => true, // see withFollowRedirects() instead * 'maxRedirects' => 10, // see withFollowRedirects() instead * 'obeySuccessCode' => true, // see withRejectErrorResponse() instead * 'streaming' => false, // deprecated, see requestStreaming() instead - * )); + * ]); * ``` * * See also [timeouts](#timeouts), [redirects](#redirects) and @@ -814,7 +814,7 @@ private function withOptions(array $options) * @param string|ReadableStreamInterface $body * @return PromiseInterface */ - private function requestMayBeStreaming($method, $url, array $headers = array(), $body = '') + private function requestMayBeStreaming($method, $url, array $headers = [], $body = '') { if ($this->baseUrl !== null) { // ensure we're actually below the base URL diff --git a/src/HttpServer.php b/src/HttpServer.php index cd0874cc..24168cc5 100644 --- a/src/HttpServer.php +++ b/src/HttpServer.php @@ -27,9 +27,9 @@ * $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { * return new React\Http\Message\Response( * React\Http\Message\Response::STATUS_OK, - * array( + * [ * 'Content-Type' => 'text/plain' - * ), + * ], * "Hello World!\n" * ); * }); @@ -222,7 +222,7 @@ public function __construct($requestHandlerOrLoop) } } - $middleware = array(); + $middleware = []; if (!$streaming) { $maxSize = $this->getMaxRequestSize(); $concurrency = $this->getConcurrentRequestsLimit(\ini_get('memory_limit'), $maxSize); @@ -253,9 +253,8 @@ public function __construct($requestHandlerOrLoop) $this->streamingServer = new StreamingServer($loop, new MiddlewareRunner($middleware)); - $that = $this; - $this->streamingServer->on('error', function ($error) use ($that) { - $that->emit('error', array($error)); + $this->streamingServer->on('error', function ($error) { + $this->emit('error', [$error]); }); } @@ -299,11 +298,11 @@ public function __construct($requestHandlerOrLoop) * ```php * $http = new React\Http\HttpServer($handler); * - * $socket = new React\Socket\SocketServer('tls://0.0.0.0:8443', array( - * 'tls' => array( + * $socket = new React\Socket\SocketServer('tls://0.0.0.0:8443', [ + * 'tls' => [ * 'local_cert' => __DIR__ . '/localhost.pem' - * ) - * )); + * ] + * ]); * $http->listen($socket); * ``` * @@ -340,7 +339,7 @@ private function getConcurrentRequestsLimit($memory_limit, $post_max_size) */ private function getMaxRequestSize($post_max_size = null) { - $maxSize = IniUtil::iniSizeToBytes($post_max_size === null ? \ini_get('post_max_size') : $post_max_size); + $maxSize = IniUtil::iniSizeToBytes($post_max_size ?? \ini_get('post_max_size')); return ($maxSize === 0 || $maxSize >= self::MAXIMUM_BUFFER_SIZE) ? self::MAXIMUM_BUFFER_SIZE : $maxSize; } diff --git a/src/Io/AbstractMessage.php b/src/Io/AbstractMessage.php index a0706bb1..232a5442 100644 --- a/src/Io/AbstractMessage.php +++ b/src/Io/AbstractMessage.php @@ -22,10 +22,10 @@ abstract class AbstractMessage implements MessageInterface const REGEX_HEADERS = '/^([^()<>@,;:\\\"\/\[\]?={}\x00-\x20\x7F]++):[\x20\x09]*+((?:[\x20\x09]*+[\x21-\x7E\x80-\xFF]++)*+)[\x20\x09]*+[\r]?+\n/m'; /** @var array */ - private $headers = array(); + private $headers = []; /** @var array */ - private $headerNamesLowerCase = array(); + private $headerNamesLowerCase = []; /** @var string */ private $protocolVersion; @@ -41,13 +41,13 @@ abstract class AbstractMessage implements MessageInterface protected function __construct($protocolVersion, array $headers, StreamInterface $body) { foreach ($headers as $name => $value) { - if ($value !== array()) { + if ($value !== []) { if (\is_array($value)) { foreach ($value as &$one) { $one = (string) $one; } } else { - $value = array((string) $value); + $value = [(string) $value]; } $lower = \strtolower($name); @@ -95,7 +95,7 @@ public function hasHeader($name) public function getHeader($name) { $lower = \strtolower($name); - return isset($this->headerNamesLowerCase[$lower]) ? $this->headers[$this->headerNamesLowerCase[$lower]] : array(); + return isset($this->headerNamesLowerCase[$lower]) ? $this->headers[$this->headerNamesLowerCase[$lower]] : []; } public function getHeaderLine($name) @@ -105,14 +105,14 @@ public function getHeaderLine($name) public function withHeader($name, $value) { - if ($value === array()) { + if ($value === []) { return $this->withoutHeader($name); } elseif (\is_array($value)) { foreach ($value as &$one) { $one = (string) $one; } } else { - $value = array((string) $value); + $value = [(string) $value]; } $lower = \strtolower($name); @@ -133,11 +133,11 @@ public function withHeader($name, $value) public function withAddedHeader($name, $value) { - if ($value === array()) { + if ($value === []) { return $this; } - return $this->withHeader($name, \array_merge($this->getHeader($name), \is_array($value) ? $value : array($value))); + return $this->withHeader($name, \array_merge($this->getHeader($name), \is_array($value) ? $value : [$value])); } public function withoutHeader($name) diff --git a/src/Io/AbstractRequest.php b/src/Io/AbstractRequest.php index f32307f7..1182f7ab 100644 --- a/src/Io/AbstractRequest.php +++ b/src/Io/AbstractRequest.php @@ -50,7 +50,7 @@ protected function __construct( $host = $uri->getHost(); if ($host !== '') { foreach ($headers as $name => $value) { - if (\strtolower($name) === 'host' && $value !== array()) { + if (\strtolower($name) === 'host' && $value !== []) { $host = ''; break; } @@ -61,7 +61,7 @@ protected function __construct( $host .= ':' . $port; } - $headers = array('Host' => $host) + $headers; + $headers = ['Host' => $host] + $headers; } } diff --git a/src/Io/BufferedBody.php b/src/Io/BufferedBody.php index 4a4d8393..9b1d9887 100644 --- a/src/Io/BufferedBody.php +++ b/src/Io/BufferedBody.php @@ -174,6 +174,6 @@ public function getContents() public function getMetadata($key = null) { - return $key === null ? array() : null; + return $key === null ? [] : null; } } diff --git a/src/Io/ChunkedDecoder.php b/src/Io/ChunkedDecoder.php index 2f58f42b..996484db 100644 --- a/src/Io/ChunkedDecoder.php +++ b/src/Io/ChunkedDecoder.php @@ -31,10 +31,10 @@ public function __construct(ReadableStreamInterface $input) { $this->input = $input; - $this->input->on('data', array($this, 'handleData')); - $this->input->on('end', array($this, 'handleEnd')); - $this->input->on('error', array($this, 'handleError')); - $this->input->on('close', array($this, 'close')); + $this->input->on('data', [$this, 'handleData']); + $this->input->on('end', [$this, 'handleEnd']); + $this->input->on('error', [$this, 'handleError']); + $this->input->on('close', [$this, 'close']); } public function isReadable() @@ -52,7 +52,7 @@ public function resume() $this->input->resume(); } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { Util::pipe($this, $dest, $options); @@ -86,7 +86,7 @@ public function handleEnd() /** @internal */ public function handleError(Exception $e) { - $this->emit('error', array($e)); + $this->emit('error', [$e]); $this->close(); } @@ -139,7 +139,7 @@ public function handleData($data) if ($chunk !== '') { $this->transferredSize += \strlen($chunk); - $this->emit('data', array($chunk)); + $this->emit('data', [$chunk]); $this->buffer = (string)\substr($this->buffer, \strlen($chunk)); } diff --git a/src/Io/ChunkedEncoder.php b/src/Io/ChunkedEncoder.php index c84ef54f..0bfe34f8 100644 --- a/src/Io/ChunkedEncoder.php +++ b/src/Io/ChunkedEncoder.php @@ -23,10 +23,10 @@ public function __construct(ReadableStreamInterface $input) { $this->input = $input; - $this->input->on('data', array($this, 'handleData')); - $this->input->on('end', array($this, 'handleEnd')); - $this->input->on('error', array($this, 'handleError')); - $this->input->on('close', array($this, 'close')); + $this->input->on('data', [$this, 'handleData']); + $this->input->on('end', [$this, 'handleEnd']); + $this->input->on('error', [$this, 'handleError']); + $this->input->on('close', [$this, 'close']); } public function isReadable() @@ -44,7 +44,7 @@ public function resume() $this->input->resume(); } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { return Util::pipe($this, $dest, $options); } @@ -66,23 +66,23 @@ public function close() public function handleData($data) { if ($data !== '') { - $this->emit('data', array( + $this->emit('data', [ \dechex(\strlen($data)) . "\r\n" . $data . "\r\n" - )); + ]); } } /** @internal */ public function handleError(\Exception $e) { - $this->emit('error', array($e)); + $this->emit('error', [$e]); $this->close(); } /** @internal */ public function handleEnd() { - $this->emit('data', array("0\r\n\r\n")); + $this->emit('data', ["0\r\n\r\n"]); if (!$this->closed) { $this->emit('end'); diff --git a/src/Io/ClientConnectionManager.php b/src/Io/ClientConnectionManager.php index faac98b6..794c0340 100644 --- a/src/Io/ClientConnectionManager.php +++ b/src/Io/ClientConnectionManager.php @@ -8,6 +8,8 @@ use React\Promise\PromiseInterface; use React\Socket\ConnectionInterface; use React\Socket\ConnectorInterface; +use function React\Promise\reject; +use function React\Promise\resolve; /** * [Internal] Manages outgoing HTTP connections for the HTTP client @@ -24,16 +26,16 @@ class ClientConnectionManager private $loop; /** @var string[] */ - private $idleUris = array(); + private $idleUris = []; /** @var ConnectionInterface[] */ - private $idleConnections = array(); + private $idleConnections = []; /** @var TimerInterface[] */ - private $idleTimers = array(); + private $idleTimers = []; /** @var \Closure[] */ - private $idleStreamHandlers = array(); + private $idleStreamHandlers = []; /** @var float */ private $maximumTimeToKeepAliveIdleConnection = 0.001; @@ -51,7 +53,7 @@ public function connect(UriInterface $uri) { $scheme = $uri->getScheme(); if ($scheme !== 'https' && $scheme !== 'http') { - return \React\Promise\reject(new \InvalidArgumentException( + return reject(new \InvalidArgumentException( 'Invalid request URL given' )); } @@ -74,7 +76,7 @@ public function connect(UriInterface $uri) $this->loop->cancelTimer($this->idleTimers[$id]); unset($this->idleUris[$id], $this->idleConnections[$id], $this->idleTimers[$id], $this->idleStreamHandlers[$id]); - return \React\Promise\resolve($connection); + return resolve($connection); } } @@ -100,10 +102,8 @@ public function keepAlive(UriInterface $uri, ConnectionInterface $connection) $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); + $cleanUp = function () use ($connection) { + $this->cleanUpConnection($connection); }; // clean up and close connection when maximum time to keep-alive idle connection has passed @@ -116,11 +116,8 @@ public function keepAlive(UriInterface $uri, ConnectionInterface $connection) $connection->on('error', $cleanUp); } - /** - * @internal - * @return void - */ - public function cleanUpConnection(ConnectionInterface $connection) // private (PHP 5.4+) + /** @return void */ + private function cleanUpConnection(ConnectionInterface $connection) { $id = \array_search($connection, $this->idleConnections, true); if ($id === false) { diff --git a/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index 3bdf9b1f..fb0cd3e5 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -74,38 +74,32 @@ private function writeHead() 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); + function (ConnectionInterface $connection) use ($headers) { + $this->connection = $connection; - $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')); + $connection->on('drain', [$this, 'handleDrain']); + $connection->on('data', [$this, 'handleData']); + $connection->on('end', [$this, 'handleEnd']); + $connection->on('error', [$this, 'handleError']); + $connection->on('close', [$this, 'close']); - $more = $connection->write($headers . "\r\n" . $pendingWrites); + $more = $connection->write($headers . "\r\n" . $this->pendingWrites); - assert($stateRef === ClientRequestStream::STATE_WRITING_HEAD); - $stateRef = ClientRequestStream::STATE_HEAD_WRITTEN; + assert($this->state === ClientRequestStream::STATE_WRITING_HEAD); + $this->state = ClientRequestStream::STATE_HEAD_WRITTEN; // clear pending writes if non-empty - if ($pendingWrites !== '') { - $pendingWrites = ''; + if ($this->pendingWrites !== '') { + $this->pendingWrites = ''; if ($more) { - $that->emit('drain'); + $this->emit('drain'); } } }, - array($this, 'closeError') + [$this, 'closeError'] ); $this->on('close', function() use ($promise) { @@ -179,29 +173,26 @@ public function handleData($data) // 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')); + $connection->removeListener('drain', [$this, 'handleDrain']); + $connection->removeListener('data', [$this, 'handleData']); + $connection->removeListener('end', [$this, 'handleEnd']); + $connection->removeListener('error', [$this, 'handleError']); + $connection->removeListener('close', [$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) { + $input->on('close', function () use ($connection, $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); + if ($successfulEndReceived && $connection->isReadable() && $this->hasMessageKeepAliveEnabled($response) && $this->hasMessageKeepAliveEnabled($this->request)) { + $this->connectionManager->keepAlive($this->request->getUri(), $connection); } else { $connection->close(); } - $that->close(); + $this->close(); }); // determine length of response body @@ -220,7 +211,7 @@ public function handleData($data) }); // emit response with streaming response body (see `Sender`) - $this->emit('response', array($response, $body)); + $this->emit('response', [$response, $body]); // re-emit HTTP response body to trigger body parsing if parts of it are buffered if ($bodyChunk !== '') { @@ -255,7 +246,7 @@ public function closeError(\Exception $error) if (self::STATE_END <= $this->state) { return; } - $this->emit('error', array($error)); + $this->emit('error', [$error]); $this->close(); } diff --git a/src/Io/Clock.php b/src/Io/Clock.php index 92c1cb09..c2445a94 100644 --- a/src/Io/Clock.php +++ b/src/Io/Clock.php @@ -42,10 +42,9 @@ public function now() $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; + $this->loop->futureTick(function () { + assert($this->now !== null); + $this->now = null; }); } diff --git a/src/Io/CloseProtectionStream.php b/src/Io/CloseProtectionStream.php index 2e1ed6e4..7fae08e7 100644 --- a/src/Io/CloseProtectionStream.php +++ b/src/Io/CloseProtectionStream.php @@ -28,10 +28,10 @@ public function __construct(ReadableStreamInterface $input) { $this->input = $input; - $this->input->on('data', array($this, 'handleData')); - $this->input->on('end', array($this, 'handleEnd')); - $this->input->on('error', array($this, 'handleError')); - $this->input->on('close', array($this, 'close')); + $this->input->on('data', [$this, 'handleData']); + $this->input->on('end', [$this, 'handleEnd']); + $this->input->on('error', [$this, 'handleError']); + $this->input->on('close', [$this, 'close']); } public function isReadable() @@ -59,7 +59,7 @@ public function resume() $this->input->resume(); } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { Util::pipe($this, $dest, $options); @@ -75,10 +75,10 @@ public function close() $this->closed = true; // stop listening for incoming events - $this->input->removeListener('data', array($this, 'handleData')); - $this->input->removeListener('error', array($this, 'handleError')); - $this->input->removeListener('end', array($this, 'handleEnd')); - $this->input->removeListener('close', array($this, 'close')); + $this->input->removeListener('data', [$this, 'handleData']); + $this->input->removeListener('error', [$this, 'handleError']); + $this->input->removeListener('end', [$this, 'handleEnd']); + $this->input->removeListener('close', [$this, 'close']); // resume the stream to ensure we discard everything from incoming connection if ($this->paused) { @@ -93,7 +93,7 @@ public function close() /** @internal */ public function handleData($data) { - $this->emit('data', array($data)); + $this->emit('data', [$data]); } /** @internal */ @@ -106,6 +106,6 @@ public function handleEnd() /** @internal */ public function handleError(\Exception $e) { - $this->emit('error', array($e)); + $this->emit('error', [$e]); } } diff --git a/src/Io/EmptyBodyStream.php b/src/Io/EmptyBodyStream.php index 5056219c..7f9c8ad0 100644 --- a/src/Io/EmptyBodyStream.php +++ b/src/Io/EmptyBodyStream.php @@ -44,7 +44,7 @@ public function resume() // NOOP } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { Util::pipe($this, $dest, $options); @@ -137,6 +137,6 @@ public function getContents() /** @ignore */ public function getMetadata($key = null) { - return ($key === null) ? array() : null; + return ($key === null) ? [] : null; } } diff --git a/src/Io/HttpBodyStream.php b/src/Io/HttpBodyStream.php index 25d15a18..8be9b854 100644 --- a/src/Io/HttpBodyStream.php +++ b/src/Io/HttpBodyStream.php @@ -39,10 +39,10 @@ public function __construct(ReadableStreamInterface $input, $size) $this->input = $input; $this->size = $size; - $this->input->on('data', array($this, 'handleData')); - $this->input->on('end', array($this, 'handleEnd')); - $this->input->on('error', array($this, 'handleError')); - $this->input->on('close', array($this, 'close')); + $this->input->on('data', [$this, 'handleData']); + $this->input->on('end', [$this, 'handleEnd']); + $this->input->on('error', [$this, 'handleError']); + $this->input->on('close', [$this, 'close']); } public function isReadable() @@ -60,7 +60,7 @@ public function resume() $this->input->resume(); } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { Util::pipe($this, $dest, $options); @@ -161,13 +161,13 @@ public function getMetadata($key = null) /** @internal */ public function handleData($data) { - $this->emit('data', array($data)); + $this->emit('data', [$data]); } /** @internal */ public function handleError(\Exception $e) { - $this->emit('error', array($e)); + $this->emit('error', [$e]); $this->close(); } diff --git a/src/Io/LengthLimitedStream.php b/src/Io/LengthLimitedStream.php index bc64c54b..c4a38b13 100644 --- a/src/Io/LengthLimitedStream.php +++ b/src/Io/LengthLimitedStream.php @@ -27,10 +27,10 @@ public function __construct(ReadableStreamInterface $stream, $maxLength) $this->stream = $stream; $this->maxLength = $maxLength; - $this->stream->on('data', array($this, 'handleData')); - $this->stream->on('end', array($this, 'handleEnd')); - $this->stream->on('error', array($this, 'handleError')); - $this->stream->on('close', array($this, 'close')); + $this->stream->on('data', [$this, 'handleData']); + $this->stream->on('end', [$this, 'handleEnd']); + $this->stream->on('error', [$this, 'handleError']); + $this->stream->on('close', [$this, 'close']); } public function isReadable() @@ -48,7 +48,7 @@ public function resume() $this->stream->resume(); } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { Util::pipe($this, $dest, $options); @@ -79,21 +79,21 @@ public function handleData($data) if ($data !== '') { $this->transferredLength += \strlen($data); - $this->emit('data', array($data)); + $this->emit('data', [$data]); } if ($this->transferredLength === $this->maxLength) { // 'Content-Length' reached, stream will end $this->emit('end'); $this->close(); - $this->stream->removeListener('data', array($this, 'handleData')); + $this->stream->removeListener('data', [$this, 'handleData']); } } /** @internal */ public function handleError(\Exception $e) { - $this->emit('error', array($e)); + $this->emit('error', [$e]); $this->close(); } diff --git a/src/Io/MiddlewareRunner.php b/src/Io/MiddlewareRunner.php index dedf6ff1..c05c5a1a 100644 --- a/src/Io/MiddlewareRunner.php +++ b/src/Io/MiddlewareRunner.php @@ -40,8 +40,7 @@ public function __invoke(ServerRequestInterface $request) return $this->call($request, 0); } - /** @internal */ - public function call(ServerRequestInterface $request, $position) + private function call(ServerRequestInterface $request, $position) { // final request handler will be invoked without a next handler if (!isset($this->middleware[$position + 1])) { @@ -49,9 +48,8 @@ public function call(ServerRequestInterface $request, $position) return $handler($request); } - $that = $this; - $next = function (ServerRequestInterface $request) use ($that, $position) { - return $that->call($request, $position + 1); + $next = function (ServerRequestInterface $request) use ($position) { + return $this->call($request, $position + 1); }; // invoke middleware request handler with next handler diff --git a/src/Io/MultipartParser.php b/src/Io/MultipartParser.php index c65bb655..cdfe189b 100644 --- a/src/Io/MultipartParser.php +++ b/src/Io/MultipartParser.php @@ -166,7 +166,7 @@ private function parsePart($chunk) $this->parseFile( $name, $filename, - isset($headers['content-type'][0]) ? $headers['content-type'][0] : null, + $headers['content-type'][0] ?? null, $body ); } else { @@ -268,7 +268,7 @@ private function parsePost($name, $value) private function parseHeaders($header) { - $headers = array(); + $headers = []; foreach (\explode("\r\n", \trim($header)) as $line) { $parts = \explode(':', $line, 2); @@ -315,12 +315,12 @@ private function extractPost($postFields, $key, $value) $previousChunkKey = $chunkKey; if ($previousChunkKey === '') { - $parent[] = array(); + $parent[] = []; \end($parent); $parent = &$parent[\key($parent)]; } else { if (!isset($parent[$previousChunkKey]) || !\is_array($parent[$previousChunkKey])) { - $parent[$previousChunkKey] = array(); + $parent[$previousChunkKey] = []; } $parent = &$parent[$previousChunkKey]; } diff --git a/src/Io/PauseBufferStream.php b/src/Io/PauseBufferStream.php index fb5ed456..b1132adc 100644 --- a/src/Io/PauseBufferStream.php +++ b/src/Io/PauseBufferStream.php @@ -36,10 +36,10 @@ public function __construct(ReadableStreamInterface $input) { $this->input = $input; - $this->input->on('data', array($this, 'handleData')); - $this->input->on('end', array($this, 'handleEnd')); - $this->input->on('error', array($this, 'handleError')); - $this->input->on('close', array($this, 'handleClose')); + $this->input->on('data', [$this, 'handleData']); + $this->input->on('end', [$this, 'handleEnd']); + $this->input->on('error', [$this, 'handleError']); + $this->input->on('close', [$this, 'handleClose']); } /** @@ -91,12 +91,12 @@ public function resume() $this->implicit = false; if ($this->dataPaused !== '') { - $this->emit('data', array($this->dataPaused)); + $this->emit('data', [$this->dataPaused]); $this->dataPaused = ''; } if ($this->errorPaused) { - $this->emit('error', array($this->errorPaused)); + $this->emit('error', [$this->errorPaused]); return $this->close(); } @@ -114,7 +114,7 @@ public function resume() $this->input->resume(); } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { Util::pipe($this, $dest, $options); @@ -146,7 +146,7 @@ public function handleData($data) return; } - $this->emit('data', array($data)); + $this->emit('data', [$data]); } /** @internal */ @@ -157,7 +157,7 @@ public function handleError(\Exception $e) return; } - $this->emit('error', array($e)); + $this->emit('error', [$e]); $this->close(); } diff --git a/src/Io/ReadableBodyStream.php b/src/Io/ReadableBodyStream.php index daef45f9..9a8bd105 100644 --- a/src/Io/ReadableBodyStream.php +++ b/src/Io/ReadableBodyStream.php @@ -23,22 +23,20 @@ public function __construct(ReadableStreamInterface $input, $size = null) $this->input = $input; $this->size = $size; - $that = $this; - $pos =& $this->position; - $input->on('data', function ($data) use ($that, &$pos, $size) { - $that->emit('data', array($data)); - - $pos += \strlen($data); - if ($size !== null && $pos >= $size) { - $that->handleEnd(); + $input->on('data', function ($data) use ($size) { + $this->emit('data', [$data]); + + $this->position += \strlen($data); + if ($size !== null && $this->position >= $size) { + $this->handleEnd(); } }); - $input->on('error', function ($error) use ($that) { - $that->emit('error', array($error)); - $that->close(); + $input->on('error', function ($error) { + $this->emit('error', [$error]); + $this->close(); }); - $input->on('end', array($that, 'handleEnd')); - $input->on('close', array($that, 'close')); + $input->on('end', [$this, 'handleEnd']); + $input->on('close', [$this, 'close']); } public function close() @@ -67,7 +65,7 @@ public function resume() $this->input->resume(); } - public function pipe(WritableStreamInterface $dest, array $options = array()) + public function pipe(WritableStreamInterface $dest, array $options = []) { Util::pipe($this, $dest, $options); @@ -136,14 +134,14 @@ public function getContents() public function getMetadata($key = null) { - return ($key === null) ? array() : null; + return ($key === null) ? [] : null; } /** @internal */ public function handleEnd() { if ($this->position !== $this->size && $this->size !== null) { - $this->emit('error', array(new \UnderflowException('Unexpected end of response body after ' . $this->position . '/' . $this->size . ' bytes'))); + $this->emit('error', [new \UnderflowException('Unexpected end of response body after ' . $this->position . '/' . $this->size . ' bytes')]); } else { $this->emit('end'); } diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 8975ce57..403ab0cc 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -28,7 +28,7 @@ class RequestHeaderParser extends EventEmitter private $clock; /** @var array> */ - private $connectionParams = array(); + private $connectionParams = []; public function __construct(Clock $clock) { @@ -38,22 +38,20 @@ public function __construct(Clock $clock) public function handle(ConnectionInterface $conn) { $buffer = ''; - $maxSize = $this->maxSize; - $that = $this; - $conn->on('data', $fn = function ($data) use (&$buffer, &$fn, $conn, $maxSize, $that) { + $conn->on('data', $fn = function ($data) use (&$buffer, &$fn, $conn) { // append chunk of data to buffer and look for end of request headers $buffer .= $data; $endOfHeader = \strpos($buffer, "\r\n\r\n"); // reject request if buffer size is exceeded - if ($endOfHeader > $maxSize || ($endOfHeader === false && isset($buffer[$maxSize]))) { + if ($endOfHeader > $this->maxSize || ($endOfHeader === false && isset($buffer[$this->maxSize]))) { $conn->removeListener('data', $fn); $fn = null; - $that->emit('error', array( - new \OverflowException("Maximum header size of {$maxSize} exceeded.", Response::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE), + $this->emit('error', [ + new \OverflowException("Maximum header size of {$this->maxSize} exceeded.", Response::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE), $conn - )); + ]); return; } @@ -67,16 +65,16 @@ public function handle(ConnectionInterface $conn) $fn = null; try { - $request = $that->parseRequest( + $request = $this->parseRequest( (string)\substr($buffer, 0, $endOfHeader + 2), $conn ); } catch (Exception $exception) { $buffer = ''; - $that->emit('error', array( + $this->emit('error', [ $exception, $conn - )); + ]); return; } @@ -105,10 +103,10 @@ public function handle(ConnectionInterface $conn) $bodyBuffer = isset($buffer[$endOfHeader + 4]) ? \substr($buffer, $endOfHeader + 4) : ''; $buffer = ''; - $that->emit('headers', array($request, $conn)); + $this->emit('headers', [$request, $conn]); if ($bodyBuffer !== '') { - $conn->emit('data', array($bodyBuffer)); + $conn->emit('data', [$bodyBuffer]); } // happy path: request body is known to be empty => immediately end stream @@ -134,11 +132,11 @@ public function parseRequest($headers, ConnectionInterface $connection) $serverParams = $this->connectionParams[$cid]; } else { // assign new server params for new connection - $serverParams = array(); + $serverParams = []; // scheme is `http` unless TLS is used $localSocketUri = $connection->getLocalAddress(); - $localParts = $localSocketUri === null ? array() : \parse_url(https://github.com/$localSocketUri); + $localParts = $localSocketUri === null ? [] : \parse_url(https://github.com/$localSocketUri); if (isset($localParts['scheme']) && $localParts['scheme'] === 'tls') { $serverParams['HTTPS'] = 'on'; } @@ -162,10 +160,9 @@ public function parseRequest($headers, ConnectionInterface $connection) // 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]); + $connection->on('close', function () use ($cid) { + assert(\is_array($this->connectionParams[$cid])); + unset($this->connectionParams[$cid]); }); } diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 1d563891..8ece2ee0 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -40,7 +40,7 @@ class Sender * settings. You can use this method manually like this: * * ```php - * $connector = new \React\Socket\Connector(array(), $loop); + * $connector = new \React\Socket\Connector([], $loop); * $sender = \React\Http\Io\Sender::createFromLoop($loop, $connector); * ``` * @@ -51,7 +51,7 @@ class Sender public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector = null) { if ($connector === null) { - $connector = new Connector(array(), $loop); + $connector = new Connector([], $loop); } return new self(new HttpClient(new ClientConnectionManager($connector, $loop))); @@ -79,7 +79,7 @@ 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)); + assert(\in_array($request->getProtocolVersion(), ['1.0', '1.1'], true)); $body = $request->getBody(); $size = $body->getSize(); @@ -87,7 +87,7 @@ public function send(RequestInterface $request) if ($size !== null && $size !== 0) { // automatically assign a "Content-Length" request header if the body size is known and non-empty $request = $request->withHeader('Content-Length', (string)$size); - } elseif ($size === 0 && \in_array($request->getMethod(), array('POST', 'PUT', 'PATCH'))) { + } elseif ($size === 0 && \in_array($request->getMethod(), ['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 && $size !== 0 && $body->isReadable() && !$request->hasHeader('Content-Length')) { diff --git a/src/Io/StreamingServer.php b/src/Io/StreamingServer.php index eee9f900..6d12d359 100644 --- a/src/Io/StreamingServer.php +++ b/src/Io/StreamingServer.php @@ -8,12 +8,13 @@ use React\EventLoop\LoopInterface; use React\Http\Message\Response; use React\Http\Message\ServerRequest; -use React\Promise; use React\Promise\PromiseInterface; use React\Socket\ConnectionInterface; use React\Socket\ServerInterface; use React\Stream\ReadableStreamInterface; use React\Stream\WritableStreamInterface; +use function React\Promise\reject; +use function React\Promise\resolve; /** * The internal `StreamingServer` class is responsible for handling incoming connections and then @@ -31,9 +32,9 @@ * $server = new StreamingServer($loop, function (ServerRequestInterface $request) { * return new Response( * Response::STATUS_OK, - * array( + * [ * 'Content-Type' => 'text/plain' - * ), + * ], * "Hello World!\n" * ); * }); @@ -55,7 +56,7 @@ * ```php * $server = new StreamingServer($loop, $handler); * - * $socket = new React\Socket\SocketServer('0.0.0.0:8080', array(), $loop); + * $socket = new React\Socket\SocketServer('0.0.0.0:8080', [], $loop); * $server->listen($socket); * ``` * @@ -109,16 +110,15 @@ public function __construct(LoopInterface $loop, $requestHandler) $this->clock = new Clock($loop); $this->parser = new RequestHeaderParser($this->clock); - $that = $this; - $this->parser->on('headers', function (ServerRequestInterface $request, ConnectionInterface $conn) use ($that) { - $that->handleRequest($conn, $request); + $this->parser->on('headers', function (ServerRequestInterface $request, ConnectionInterface $conn) { + $this->handleRequest($conn, $request); }); - $this->parser->on('error', function(\Exception $e, ConnectionInterface $conn) use ($that) { - $that->emit('error', array($e)); + $this->parser->on('error', function(\Exception $e, ConnectionInterface $conn) { + $this->emit('error', [$e]); // parsing failed => assume dummy request and send appropriate error - $that->writeError( + $this->writeError( $conn, $e->getCode() !== 0 ? $e->getCode() : Response::STATUS_BAD_REQUEST, new ServerRequest('GET', '/') @@ -134,7 +134,7 @@ public function __construct(LoopInterface $loop, $requestHandler) */ public function listen(ServerInterface $socket) { - $socket->on('connection', array($this->parser, 'handle')); + $socket->on('connection', [$this->parser, 'handle']); } /** @internal */ @@ -145,15 +145,11 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface } // execute request handler callback - $callback = $this->callback; try { - $response = $callback($request); - } catch (\Exception $error) { + $response = ($this->callback)($request); + } catch (\Throwable $error) { // request handler callback throws an Exception - $response = Promise\reject($error); - } catch (\Throwable $error) { // @codeCoverageIgnoreStart - // request handler callback throws a PHP7+ Error - $response = Promise\reject($error); // @codeCoverageIgnoreEnd + $response = reject($error); } // cancel pending promise once connection closes @@ -177,23 +173,22 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface // did not return a promise? this is an error, convert into one for rejection below. if (!$response instanceof PromiseInterface) { - $response = Promise\resolve($response); + $response = resolve($response); } - $that = $this; $response->then( - function ($response) use ($that, $conn, $request) { + function ($response) use ($conn, $request) { if (!$response instanceof ResponseInterface) { $message = 'The response callback is expected to resolve with an object implementing Psr\Http\Message\ResponseInterface, but resolved with "%s" instead.'; $message = \sprintf($message, \is_object($response) ? \get_class($response) : \gettype($response)); $exception = new \RuntimeException($message); - $that->emit('error', array($exception)); - return $that->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); + $this->emit('error', [$exception]); + return $this->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); } - $that->handleResponse($conn, $request, $response); + $this->handleResponse($conn, $request, $response); }, - function ($error) use ($that, $conn, $request) { + function ($error) use ($conn, $request) { $message = 'The response callback is expected to resolve with an object implementing Psr\Http\Message\ResponseInterface, but rejected with "%s" instead.'; $message = \sprintf($message, \is_object($error) ? \get_class($error) : \gettype($error)); @@ -205,8 +200,8 @@ function ($error) use ($that, $conn, $request) { $exception = new \RuntimeException($message, 0, $previous); - $that->emit('error', array($exception)); - return $that->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); + $this->emit('error', [$exception]); + return $this->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); } )->then($connectionOnCloseResponseCancelerHandler, $connectionOnCloseResponseCancelerHandler); } @@ -216,10 +211,10 @@ public function writeError(ConnectionInterface $conn, $code, ServerRequestInterf { $response = new Response( $code, - array( + [ 'Content-Type' => 'text/plain', 'Connection' => 'close' // we do not want to keep the connection open after an error - ), + ], 'Error ' . $code ); @@ -347,7 +342,7 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt } if ($code < 100 || $code > 999 || \substr_count($headers, "\n") !== ($expected + 1) || \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers) !== $expected) { - $this->emit('error', array(new \InvalidArgumentException('Unable to send response with invalid response headers'))); + $this->emit('error', [new \InvalidArgumentException('Unable to send response with invalid response headers')]); $this->writeError($connection, Response::STATUS_INTERNAL_SERVER_ERROR, $request); return; } @@ -387,15 +382,14 @@ public function handleResponse(ConnectionInterface $connection, ServerRequestInt // Close response stream once connection closes. // Note that this TCP/IP close detection may take some time, // in particular this may only fire on a later read/write attempt. - $connection->on('close', array($body, 'close')); + $connection->on('close', [$body, 'close']); // write streaming body and then wait for next request over persistent connection if ($persist) { - $body->pipe($connection, array('end' => false)); - $parser = $this->parser; - $body->on('end', function () use ($connection, $parser, $body) { - $connection->removeListener('close', array($body, 'close')); - $parser->handle($connection); + $body->pipe($connection, ['end' => false]); + $body->on('end', function () use ($connection, $body) { + $connection->removeListener('close', [$body, 'close']); + $this->parser->handle($connection); }); } else { $body->pipe($connection); diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index 64738f56..6790cb45 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -13,6 +13,8 @@ use React\Promise\Promise; use React\Promise\PromiseInterface; use React\Stream\ReadableStreamInterface; +use function React\Promise\reject; +use function React\Promise\resolve; /** * @internal @@ -77,21 +79,20 @@ public function send(RequestInterface $request) }); // 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")); + $timeout = (float) ($this->timeout ?? ini_get("default_socket_timeout")); - $loop = $this->loop; $this->next($request, $deferred, $state)->then( - function (ResponseInterface $response) use ($state, $deferred, $loop, &$timeout) { + function (ResponseInterface $response) use ($state, $deferred, &$timeout) { if ($state->timeout !== null) { - $loop->cancelTimer($state->timeout); + $this->loop->cancelTimer($state->timeout); $state->timeout = null; } $timeout = -1; $deferred->resolve($response); }, - function ($e) use ($state, $deferred, $loop, &$timeout) { + function ($e) use ($state, $deferred, &$timeout) { if ($state->timeout !== null) { - $loop->cancelTimer($state->timeout); + $this->loop->cancelTimer($state->timeout); $state->timeout = null; } $timeout = -1; @@ -105,10 +106,9 @@ function ($e) use ($state, $deferred, $loop, &$timeout) { $body = $request->getBody(); if ($body instanceof ReadableStreamInterface && $body->isReadable()) { - $that = $this; - $body->on('close', function () use ($that, $deferred, $state, &$timeout) { + $body->on('close', function () use ($deferred, $state, &$timeout) { if ($timeout >= 0) { - $that->applyTimeout($deferred, $state, $timeout); + $this->applyTimeout($deferred, $state, $timeout); } }); } else { @@ -138,24 +138,23 @@ public function applyTimeout(Deferred $deferred, ClientRequestState $state, $tim private function next(RequestInterface $request, Deferred $deferred, ClientRequestState $state) { - $this->progress('request', array($request)); + $this->progress('request', [$request]); - $that = $this; ++$state->numRequests; $promise = $this->sender->send($request); if (!$this->streaming) { - $promise = $promise->then(function ($response) use ($deferred, $state, $that) { - return $that->bufferResponse($response, $deferred, $state); + $promise = $promise->then(function ($response) use ($deferred, $state) { + return $this->bufferResponse($response, $deferred, $state); }); } $state->pending = $promise; return $promise->then( - function (ResponseInterface $response) use ($request, $that, $deferred, $state) { - return $that->onResponse($response, $request, $deferred, $state); + function (ResponseInterface $response) use ($request, $deferred, $state) { + return $this->onResponse($response, $request, $deferred, $state); } ); } @@ -171,7 +170,7 @@ public function bufferResponse(ResponseInterface $response, Deferred $deferred, if ($size !== null && $size > $this->maximumSize) { $body->close(); - return \React\Promise\reject(new \OverflowException( + return reject(new \OverflowException( 'Response body size of ' . $size . ' bytes exceeds maximum of ' . $this->maximumSize . ' bytes', \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90 )); @@ -179,33 +178,32 @@ public function bufferResponse(ResponseInterface $response, Deferred $deferred, // body is not streaming => already buffered if (!$body instanceof ReadableStreamInterface) { - return \React\Promise\resolve($response); + return resolve($response); } /** @var ?\Closure $closer */ $closer = null; - $maximumSize = $this->maximumSize; - return $state->pending = new Promise(function ($resolve, $reject) use ($body, $maximumSize, $response, &$closer) { + return $state->pending = new Promise(function ($resolve, $reject) use ($body, $response, &$closer) { // resolve with current buffer when stream closes successfully $buffer = ''; - $body->on('close', $closer = function () use (&$buffer, $response, $maximumSize, $resolve, $reject) { + $body->on('close', $closer = function () use (&$buffer, $response, $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) { + $body->on('data', function ($data) use (&$buffer, $body, $closer, $reject) { $buffer .= $data; // close stream and reject promise if limit is exceeded - if (isset($buffer[$maximumSize])) { + if (isset($buffer[$this->maximumSize])) { $buffer = ''; assert($closer instanceof \Closure); $body->removeListener('close', $closer); $body->close(); $reject(new \OverflowException( - 'Response body size exceeds maximum of ' . $maximumSize . ' bytes', + 'Response body size exceeds maximum of ' . $this->maximumSize . ' bytes', \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90 )); } @@ -236,7 +234,7 @@ public function bufferResponse(ResponseInterface $response, Deferred $deferred, */ public function onResponse(ResponseInterface $response, RequestInterface $request, Deferred $deferred, ClientRequestState $state) { - $this->progress('response', array($response, $request)); + $this->progress('response', [$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 @@ -267,7 +265,7 @@ private function onResponseRedirect(ResponseInterface $response, RequestInterfac $location = Uri::resolve($request->getUri(), new Uri($response->getHeaderLine('Location'))); $request = $this->makeRedirectRequest($request, $location, $response->getStatusCode()); - $this->progress('redirect', array($request)); + $this->progress('redirect', [$request]); if ($state->numRequests >= $this->maxRedirects) { throw new \RuntimeException('Maximum number of redirects (' . $this->maxRedirects . ') exceeded'); @@ -308,7 +306,7 @@ private function makeRedirectRequest(RequestInterface $request, UriInterface $lo return $request; } - private function progress($name, array $args = array()) + private function progress($name, array $args = []) { return; diff --git a/src/Io/UploadedFile.php b/src/Io/UploadedFile.php index f2a6c9e7..b0d0dd98 100644 --- a/src/Io/UploadedFile.php +++ b/src/Io/UploadedFile.php @@ -57,7 +57,7 @@ public function __construct(StreamInterface $stream, $size, $error, $filename, $ $this->stream = $stream; $this->size = $size; - if (!\is_int($error) || !\in_array($error, array( + if (!\is_int($error) || !\in_array($error, [ \UPLOAD_ERR_OK, \UPLOAD_ERR_INI_SIZE, \UPLOAD_ERR_FORM_SIZE, @@ -66,7 +66,7 @@ public function __construct(StreamInterface $stream, $size, $error, $filename, $ \UPLOAD_ERR_NO_TMP_DIR, \UPLOAD_ERR_CANT_WRITE, \UPLOAD_ERR_EXTENSION, - ))) { + ])) { throw new InvalidArgumentException( 'Invalid error code, must be an UPLOAD_ERR_* constant' ); diff --git a/src/Message/Request.php b/src/Message/Request.php index 3de8c1b3..fdba39f5 100644 --- a/src/Message/Request.php +++ b/src/Message/Request.php @@ -40,7 +40,7 @@ final class Request extends AbstractRequest implements RequestInterface public function __construct( $method, $url, - array $headers = array(), + array $headers = [], $body = '', $version = '1.1' ) { diff --git a/src/Message/Response.php b/src/Message/Response.php index 107508a9..93557fab 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -16,9 +16,9 @@ * ```php * $response = new React\Http\Message\Response( * React\Http\Message\Response::STATUS_OK, - * array( + * [ * 'Content-Type' => 'text/html' - * ), + * ], * "Hello world!\n" * ); * ``` @@ -90,7 +90,7 @@ final class Response extends AbstractMessage implements ResponseInterface, Statu */ public static function html($html) { - return new self(self::STATUS_OK, array('Content-Type' => 'text/html; charset=utf-8'), $html); + return new self(self::STATUS_OK, ['Content-Type' => 'text/html; charset=utf-8'], $html); } /** @@ -124,10 +124,9 @@ public static function html($html) * fails, this method will throw an `InvalidArgumentException`. * * By default, the given structured data will be encoded with the flags as - * shown above. This includes pretty printing (PHP 5.4+) and preserving - * zero fractions for `float` values (PHP 5.6.6+) to ease debugging. It is - * assumed any additional data overhead is usually compensated by using HTTP - * response compression. + * shown above. This includes pretty printing and preserving zero fractions + * for `float` values to ease debugging. It is assumed any additional data + * overhead is usually compensated by using HTTP response compression. * * If you want to use a different status code or custom HTTP response * headers, you can manipulate the returned response object using the @@ -146,19 +145,19 @@ public static function html($html) */ public static function json($data) { - $json = @\json_encode( + $json = \json_encode( $data, - (\defined('JSON_PRETTY_PRINT') ? \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE : 0) | (\defined('JSON_PRESERVE_ZERO_FRACTION') ? \JSON_PRESERVE_ZERO_FRACTION : 0) + \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_PRESERVE_ZERO_FRACTION ); if ($json === false) { throw new \InvalidArgumentException( - 'Unable to encode given data as JSON' . (\function_exists('json_last_error_msg') ? ': ' . \json_last_error_msg() : ''), + 'Unable to encode given data as JSON: ' . \json_last_error_msg(), \json_last_error() ); } - return new self(self::STATUS_OK, array('Content-Type' => 'application/json'), $json . "\n"); + return new self(self::STATUS_OK, ['Content-Type' => 'application/json'], $json . "\n"); } /** @@ -201,7 +200,7 @@ public static function json($data) */ public static function plaintext($text) { - return new self(self::STATUS_OK, array('Content-Type' => 'text/plain; charset=utf-8'), $text); + return new self(self::STATUS_OK, ['Content-Type' => 'text/plain; charset=utf-8'], $text); } /** @@ -253,7 +252,7 @@ public static function plaintext($text) */ public static function xml($xml) { - return new self(self::STATUS_OK, array('Content-Type' => 'application/xml'), $xml); + return new self(self::STATUS_OK, ['Content-Type' => 'application/xml'], $xml); } /** @@ -275,7 +274,7 @@ public static function xml($xml) * @see self::STATUS_* * @see self::getReasonPhraseForStatusCode() */ - private static $phrasesMap = array( + private static $phrasesMap = [ 200 => 'OK', 203 => 'Non-Authoritative Information', 207 => 'Multi-Status', @@ -283,7 +282,7 @@ public static function xml($xml) 414 => 'URI Too Large', 418 => 'I\'m a teapot', 505 => 'HTTP Version Not Supported' - ); + ]; /** @var int */ private $statusCode; @@ -301,7 +300,7 @@ public static function xml($xml) */ public function __construct( $status = self::STATUS_OK, - array $headers = array(), + array $headers = [], $body = '', $version = '1.1', $reason = null @@ -366,7 +365,7 @@ private static function getReasonPhraseForStatusCode($code) } } - return isset(self::$phrasesMap[$code]) ? self::$phrasesMap[$code] : ''; + return self::$phrasesMap[$code] ?? ''; } /** @@ -379,7 +378,7 @@ private static function getReasonPhraseForStatusCode($code) */ public static function parseMessage($message) { - $start = array(); + $start = []; if (!\preg_match('#^HTTP/(?\d\.\d) (?\d{3})(?: (?[^\r\n]*+))?[\r]?+\n#m', $message, $start)) { throw new \InvalidArgumentException('Unable to parse invalid status-line'); } @@ -390,14 +389,14 @@ public static function parseMessage($message) } // check number of valid header fields matches number of lines + status line - $matches = array(); + $matches = []; $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(); + $headers = []; foreach ($matches as $match) { $headers[$match[1]][] = $match[2]; } @@ -407,7 +406,7 @@ public static function parseMessage($message) $headers, '', $start['version'], - isset($start['reason']) ? $start['reason'] : '' + $start['reason'] ?? '' ); } } diff --git a/src/Message/ServerRequest.php b/src/Message/ServerRequest.php index 32a0f62f..da0d76ab 100644 --- a/src/Message/ServerRequest.php +++ b/src/Message/ServerRequest.php @@ -31,12 +31,12 @@ */ final class ServerRequest extends AbstractRequest implements ServerRequestInterface { - private $attributes = array(); + private $attributes = []; private $serverParams; - private $fileParams = array(); - private $cookies = array(); - private $queryParams = array(); + private $fileParams = []; + private $cookies = []; + private $queryParams = []; private $parsedBody; /** @@ -51,10 +51,10 @@ final class ServerRequest extends AbstractRequest implements ServerRequestInterf public function __construct( $method, $url, - array $headers = array(), + array $headers = [], $body = '', $version = '1.1', - $serverParams = array() + $serverParams = [] ) { if (\is_string($body)) { $body = new BufferedBody($body); @@ -174,7 +174,7 @@ public function withoutAttribute($name) private function parseCookie($cookie) { $cookieArray = \explode(';', $cookie); - $result = array(); + $result = []; foreach ($cookieArray as $pair) { $pair = \trim($pair); @@ -202,7 +202,7 @@ private function parseCookie($cookie) public static function parseMessage($message, array $serverParams) { // parse request line like "GET /path HTTP/1.1" - $start = array(); + $start = []; if (!\preg_match('#^(?[^ ]+) (?[^ ]+) HTTP/(?\d\.\d)#m', $message, $start)) { throw new \InvalidArgumentException('Unable to parse invalid request-line'); } @@ -213,7 +213,7 @@ public static function parseMessage($message, array $serverParams) } // check number of valid header fields matches number of lines + request line - $matches = array(); + $matches = []; $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'); @@ -221,7 +221,7 @@ public static function parseMessage($message, array $serverParams) // format all header fields into associative array $host = null; - $headers = array(); + $headers = []; foreach ($matches as $match) { $headers[$match[1]][] = $match[2]; diff --git a/src/Message/Uri.php b/src/Message/Uri.php index 6b77d3e7..84fc38d8 100644 --- a/src/Message/Uri.php +++ b/src/Message/Uri.php @@ -54,8 +54,8 @@ public function __construct($uri) $this->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['user'])) { + $this->userInfo = $this->encode($parts['user'], \PHP_URL_USER) . (isset($parts['pass']) ? ':' . $this->encode($parts['pass'], \PHP_URL_PASS) : ''); } if (isset($parts['host'])) { @@ -301,7 +301,7 @@ public static function resolve(UriInterface $base, UriInterface $rel) 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()); + $new = $base->withUserInfo($userInfo[0], $userInfo[1] ?? null)->withHost($rel->getHost())->withPort($rel->getPort()); } if ($reset && $rel->getPath() === '') { @@ -334,7 +334,7 @@ public static function resolve(UriInterface $base, UriInterface $rel) */ private static function removeDotSegments($path) { - $segments = array(); + $segments = []; foreach (\explode('/', $path) as $segment) { if ($segment === '..') { \array_pop($segments); diff --git a/src/Middleware/LimitConcurrentRequestsMiddleware.php b/src/Middleware/LimitConcurrentRequestsMiddleware.php index b1c00da0..41477f91 100644 --- a/src/Middleware/LimitConcurrentRequestsMiddleware.php +++ b/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -6,10 +6,11 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\Io\HttpBodyStream; use React\Http\Io\PauseBufferStream; -use React\Promise; use React\Promise\PromiseInterface; use React\Promise\Deferred; use React\Stream\ReadableStreamInterface; +use function React\Promise\reject; +use function React\Promise\resolve; /** * Limits how many next handlers can be executed concurrently. @@ -71,7 +72,7 @@ final class LimitConcurrentRequestsMiddleware { private $limit; private $pending = 0; - private $queue = array(); + private $queue = []; /** * @param int $limit Maximum amount of concurrent requests handled. @@ -92,13 +93,9 @@ public function __invoke(ServerRequestInterface $request, $next) try { $response = $next($request); - } catch (\Exception $e) { + } catch (\Throwable $e) { $this->processQueue(); throw $e; - } catch (\Throwable $e) { // @codeCoverageIgnoreStart - // handle Errors just like Exceptions (PHP 7+ only) - $this->processQueue(); - throw $e; // @codeCoverageIgnoreEnd } // happy path: if next request handler returned immediately, @@ -110,7 +107,7 @@ public function __invoke(ServerRequestInterface $request, $next) // if the next handler returns a pending promise, we have to // await its resolution before invoking next queued request - return $this->await(Promise\resolve($response)); + return $this->await(resolve($response)); } // if we reach this point, then this request will need to be queued @@ -130,36 +127,29 @@ public function __invoke(ServerRequestInterface $request, $next) } // get next queue position - $queue =& $this->queue; - $queue[] = null; - \end($queue); - $id = \key($queue); + $this->queue[] = null; + \end($this->queue); + $id = \key($this->queue); - $deferred = new Deferred(function ($_, $reject) use (&$queue, $id) { + $deferred = new Deferred(function ($_, $reject) use ($id) { // queued promise cancelled before its next handler is invoked // remove from queue and reject explicitly - unset($queue[$id]); + unset($this->queue[$id]); $reject(new \RuntimeException('Cancelled queued next handler')); }); // queue request and process queue if pending does not exceed limit - $queue[$id] = $deferred; + $this->queue[$id] = $deferred; - $pending = &$this->pending; - $that = $this; - return $deferred->promise()->then(function () use ($request, $next, $body, &$pending, $that) { + return $deferred->promise()->then(function () use ($request, $next, $body) { // invoke next request handler - ++$pending; + ++$this->pending; try { $response = $next($request); - } catch (\Exception $e) { - $that->processQueue(); + } catch (\Throwable $e) { + $this->processQueue(); throw $e; - } catch (\Throwable $e) { // @codeCoverageIgnoreStart - // handle Errors just like Exceptions (PHP 7+ only) - $that->processQueue(); - throw $e; // @codeCoverageIgnoreEnd } // resume readable stream and replay buffered events @@ -169,27 +159,24 @@ public function __invoke(ServerRequestInterface $request, $next) // if the next handler returns a pending promise, we have to // await its resolution before invoking next queued request - return $that->await(Promise\resolve($response)); + return $this->await(resolve($response)); }); } /** - * @internal * @param PromiseInterface $promise * @return PromiseInterface */ - public function await(PromiseInterface $promise) + private function await(PromiseInterface $promise) { - $that = $this; - - return $promise->then(function ($response) use ($that) { - $that->processQueue(); + return $promise->then(function ($response) { + $this->processQueue(); return $response; - }, function ($error) use ($that) { - $that->processQueue(); + }, function ($error) { + $this->processQueue(); - return Promise\reject($error); + return reject($error); }); } diff --git a/src/Middleware/RequestBodyBufferMiddleware.php b/src/Middleware/RequestBodyBufferMiddleware.php index ddb39f5e..ea889bd3 100644 --- a/src/Middleware/RequestBodyBufferMiddleware.php +++ b/src/Middleware/RequestBodyBufferMiddleware.php @@ -76,11 +76,8 @@ public function __invoke(ServerRequestInterface $request, $next) try { // resolve with result of next handler $resolve($next($request->withBody(new BufferedBody($buffer)))); - } catch (\Exception $e) { + } catch (\Throwable $e) { $reject($e); - } catch (\Throwable $e) { // @codeCoverageIgnoreStart - // reject Errors just like Exceptions (PHP 7+) - $reject($e); // @codeCoverageIgnoreEnd } }); diff --git a/src/Middleware/RequestBodyParserMiddleware.php b/src/Middleware/RequestBodyParserMiddleware.php index be5ba16f..63013337 100644 --- a/src/Middleware/RequestBodyParserMiddleware.php +++ b/src/Middleware/RequestBodyParserMiddleware.php @@ -21,7 +21,7 @@ public function __construct($uploadMaxFilesize = null, $maxFileUploads = null) public function __invoke(ServerRequestInterface $request, $next) { $type = \strtolower($request->getHeaderLine('Content-Type')); - list ($type) = \explode(';', $type); + [$type] = \explode(';', $type); if ($type === 'application/x-www-form-urlencoded') { return $next($this->parseFormUrlencoded($request)); @@ -38,7 +38,7 @@ private function parseFormUrlencoded(ServerRequestInterface $request) { // parse string into array structure // ignore warnings due to excessive data structures (max_input_vars and max_input_nesting_level) - $ret = array(); + $ret = []; @\parse_str((string)$request->getBody(), $ret); return $request->withParsedBody($ret); diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index fb1a1beb..a7188b2c 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -87,9 +87,8 @@ public function testConstructWithLoopAssignsGivenLoop() public function testGetSendsGetRequest() { - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('GET', $request->getMethod()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('GET', $request->getMethod()); return true; }))->willReturn(new Promise(function () { })); @@ -98,9 +97,8 @@ public function testGetSendsGetRequest() public function testPostSendsPostRequest() { - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('POST', $request->getMethod()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('POST', $request->getMethod()); return true; }))->willReturn(new Promise(function () { })); @@ -109,9 +107,8 @@ public function testPostSendsPostRequest() public function testHeadSendsHeadRequest() { - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('HEAD', $request->getMethod()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('HEAD', $request->getMethod()); return true; }))->willReturn(new Promise(function () { })); @@ -120,9 +117,8 @@ public function testHeadSendsHeadRequest() public function testPatchSendsPatchRequest() { - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('PATCH', $request->getMethod()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('PATCH', $request->getMethod()); return true; }))->willReturn(new Promise(function () { })); @@ -131,9 +127,8 @@ public function testPatchSendsPatchRequest() public function testPutSendsPutRequest() { - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('PUT', $request->getMethod()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('PUT', $request->getMethod()); return true; }))->willReturn(new Promise(function () { })); @@ -142,9 +137,8 @@ public function testPutSendsPutRequest() public function testDeleteSendsDeleteRequest() { - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('DELETE', $request->getMethod()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('DELETE', $request->getMethod()); return true; }))->willReturn(new Promise(function () { })); @@ -153,11 +147,10 @@ public function testDeleteSendsDeleteRequest() public function testRequestOptionsSendsPutRequestWithStreamingExplicitlyDisabled() { - $this->sender->expects($this->once())->method('withOptions')->with(array('streaming' => false))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['streaming' => false])->willReturnSelf(); - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('OPTIONS', $request->getMethod()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('OPTIONS', $request->getMethod()); return true; }))->willReturn(new Promise(function () { })); @@ -166,11 +159,10 @@ public function testRequestOptionsSendsPutRequestWithStreamingExplicitlyDisabled public function testRequestStreamingGetSendsGetRequestWithStreamingExplicitlyEnabled() { - $this->sender->expects($this->once())->method('withOptions')->with(array('streaming' => true))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['streaming' => true])->willReturnSelf(); - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('GET', $request->getMethod()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('GET', $request->getMethod()); return true; }))->willReturn(new Promise(function () { })); @@ -179,77 +171,77 @@ public function testRequestStreamingGetSendsGetRequestWithStreamingExplicitlyEna public function testWithTimeoutTrueSetsDefaultTimeoutOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('timeout' => null))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['timeout' => null])->willReturnSelf(); $this->browser->withTimeout(true); } public function testWithTimeoutFalseSetsNegativeTimeoutOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('timeout' => -1))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['timeout' => -1])->willReturnSelf(); $this->browser->withTimeout(false); } public function testWithTimeout10SetsTimeoutOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('timeout' => 10))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['timeout' => 10])->willReturnSelf(); $this->browser->withTimeout(10); } public function testWithTimeoutNegativeSetsZeroTimeoutOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('timeout' => null))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['timeout' => null])->willReturnSelf(); $this->browser->withTimeout(-10); } public function testWithFollowRedirectsTrueSetsSenderOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('followRedirects' => true, 'maxRedirects' => null))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['followRedirects' => true, 'maxRedirects' => null])->willReturnSelf(); $this->browser->withFollowRedirects(true); } public function testWithFollowRedirectsFalseSetsSenderOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('followRedirects' => false, 'maxRedirects' => null))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['followRedirects' => false, 'maxRedirects' => null])->willReturnSelf(); $this->browser->withFollowRedirects(false); } public function testWithFollowRedirectsTenSetsSenderOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('followRedirects' => true, 'maxRedirects' => 10))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['followRedirects' => true, 'maxRedirects' => 10])->willReturnSelf(); $this->browser->withFollowRedirects(10); } public function testWithFollowRedirectsZeroSetsSenderOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('followRedirects' => true, 'maxRedirects' => 0))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['followRedirects' => true, 'maxRedirects' => 0])->willReturnSelf(); $this->browser->withFollowRedirects(0); } public function testWithRejectErrorResponseTrueSetsSenderOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('obeySuccessCode' => true))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['obeySuccessCode' => true])->willReturnSelf(); $this->browser->withRejectErrorResponse(true); } public function testWithRejectErrorResponseFalseSetsSenderOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('obeySuccessCode' => false))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['obeySuccessCode' => false])->willReturnSelf(); $this->browser->withRejectErrorResponse(false); } public function testWithResponseBufferThousandSetsSenderOption() { - $this->sender->expects($this->once())->method('withOptions')->with(array('maximumSize' => 1000))->willReturnSelf(); + $this->sender->expects($this->once())->method('withOptions')->with(['maximumSize' => 1000])->willReturnSelf(); $this->browser->withResponseBuffer(1000); } @@ -264,103 +256,103 @@ public function testWithBase() public function provideOtherUris() { - return array( - 'empty returns base' => array( + return [ + 'empty returns base' => [ 'http://example.com/base', '', 'http://example.com/base', - ), - 'absolute same as base returns base' => array( + ], + 'absolute same as base returns base' => [ 'http://example.com/base', 'http://example.com/base', 'http://example.com/base', - ), - 'absolute below base returns absolute' => array( + ], + 'absolute below base returns absolute' => [ 'http://example.com/base', 'http://example.com/base/another', 'http://example.com/base/another', - ), - 'slash returns base without path' => array( + ], + 'slash returns base without path' => [ 'http://example.com/base', '/', 'http://example.com/', - ), - 'relative is added behind base' => array( + ], + 'relative is added behind base' => [ 'http://example.com/base/', 'test', 'http://example.com/base/test', - ), - 'relative is added behind base without path' => array( + ], + 'relative is added behind base without path' => [ 'http://example.com/base', 'test', 'http://example.com/test', - ), - 'relative level up is added behind parent path' => array( + ], + 'relative level up is added behind parent path' => [ 'http://example.com/base/foo/', '../bar', 'http://example.com/base/bar', - ), - 'absolute with slash is added behind base without path' => array( + ], + 'absolute with slash is added behind base without path' => [ 'http://example.com/base', '/test', 'http://example.com/test', - ), - 'query string is added behind base' => array( + ], + 'query string is added behind base' => [ 'http://example.com/base', '?key=value', 'http://example.com/base?key=value', - ), - 'query string is added behind base with slash' => array( + ], + 'query string is added behind base with slash' => [ 'http://example.com/base/', '?key=value', 'http://example.com/base/?key=value', - ), - 'query string with slash is added behind base without path' => array( + ], + 'query string with slash is added behind base without path' => [ 'http://example.com/base', '/?key=value', 'http://example.com/?key=value', - ), - 'absolute with query string below base is returned as-is' => array( + ], + 'absolute with query string below base is returned as-is' => [ 'http://example.com/base', 'http://example.com/base?test', 'http://example.com/base?test', - ), - 'urlencoded special chars will stay as-is' => array( + ], + 'urlencoded special chars will stay as-is' => [ 'http://example.com/%7Bversion%7D/', '', 'http://example.com/%7Bversion%7D/' - ), - 'special chars will be urlencoded' => array( + ], + 'special chars will be urlencoded' => [ 'http://example.com/%7Bversion%7D/', '', 'http://example.com/%7Bversion%7D/' - ), - 'other domain' => array( + ], + 'other domain' => [ 'http://example.com/base/', 'http://example.org/base/', 'http://example.org/base/' - ), - 'other scheme' => array( + ], + 'other scheme' => [ 'http://example.com/base/', 'https://example.com/base/', 'https://example.com/base/' - ), - 'other port' => array( + ], + 'other port' => [ 'http://example.com/base/', 'http://example.com:81/base/', 'http://example.com:81/base/' - ), - 'other path' => array( + ], + 'other path' => [ 'http://example.com/base/', 'http://example.com/other/', 'http://example.com/other/' - ), - 'other path due to missing slash' => array( + ], + 'other path due to missing slash' => [ 'http://example.com/base/', 'http://example.com/other', 'http://example.com/other' - ), - ); + ], + ]; } /** @@ -372,9 +364,8 @@ public function testResolveUriWithBaseEndsWithoutSlash($base, $uri, $expectedAbs { $browser = $this->browser->withBase($base); - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($expectedAbsolute, $that) { - $that->assertEquals($expectedAbsolute, $request->getUri()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($expectedAbsolute) { + $this->assertEquals($expectedAbsolute, $request->getUri()); return true; }))->willReturn(new Promise(function () { })); @@ -397,9 +388,8 @@ public function testWithoutBaseFollowedByGetRequestTriesToSendIncompleteRequestU { $this->browser = $this->browser->withBase('http://example.com/')->withBase(null); - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('path', $request->getUri()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('path', $request->getUri()); return true; }))->willReturn(new Promise(function () { })); @@ -410,9 +400,8 @@ public function testWithProtocolVersionFollowedByGetRequestSendsRequestWithProto { $this->browser = $this->browser->withProtocolVersion('1.0'); - $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that) { - $that->assertEquals('1.0', $request->getProtocolVersion()); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('1.0', $request->getProtocolVersion()); return true; }))->willReturn(new Promise(function () { })); @@ -443,9 +432,8 @@ 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')); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals(['ABC'], $request->getHeader('UsEr-AgEnT')); return true; }))->willReturn(new Promise(function () { })); @@ -456,13 +444,12 @@ 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')); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals(['ABC'], $request->getHeader('UsEr-AgEnT')); return true; }))->willReturn(new Promise(function () { })); - $this->browser->get('http://example.com/', array('user-Agent' => 'ABC')); //should win + $this->browser->get('http://example.com/', ['user-Agent' => 'ABC']); //should win } public function testWithMultipleHeadersShouldBeMergedCorrectlyWithMultipleDefaultHeaders() @@ -472,28 +459,27 @@ public function testWithMultipleHeadersShouldBeMergedCorrectlyWithMultipleDefaul $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'), + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $expectedHeaders = [ + 'Host' => ['example.com'], - 'User-Test' => array('Test'), - 'just-a-header' => array('header-value'), + 'User-Test' => ['Test'], + 'just-a-header' => ['header-value'], - 'user-Agent' => array('ABC'), - 'another-header' => array('value'), - 'custom-header' => array('data'), - ); + 'user-Agent' => ['ABC'], + 'another-header' => ['value'], + 'custom-header' => ['data'], + ]; - $that->assertEquals($expectedHeaders, $request->getHeaders()); + $this->assertEquals($expectedHeaders, $request->getHeaders()); return true; }))->willReturn(new Promise(function () { })); - $headers = array( + $headers = [ '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); } @@ -502,9 +488,8 @@ 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')); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals([], $request->getHeader('user-agent')); return true; }))->willReturn(new Promise(function () { })); @@ -515,9 +500,8 @@ 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')); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals([], $request->getHeader('Connection')); return true; }))->willReturn(new Promise(function () { })); @@ -528,9 +512,8 @@ 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')); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals(['keep-alive'], $request->getHeader('Connection')); return true; }))->willReturn(new Promise(function () { })); @@ -539,9 +522,8 @@ public function testWithHeaderConnectionShouldOverwriteDefaultConnectionHeader() 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')); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals([0 => 'ReactPHP/1'], $request->getHeader('user-agent')); return true; }))->willReturn(new Promise(function () { })); @@ -552,9 +534,8 @@ public function testBrowserShouldNotSendDefaultUserAgentHeaderIfWithoutHeaderRem { $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')); + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) { + $this->assertEquals([], $request->getHeader('User-Agent')); return true; }))->willReturn(new Promise(function () { })); diff --git a/tests/Client/FunctionalIntegrationTest.php b/tests/Client/FunctionalIntegrationTest.php index 6c49c127..b727a334 100644 --- a/tests/Client/FunctionalIntegrationTest.php +++ b/tests/Client/FunctionalIntegrationTest.php @@ -7,14 +7,15 @@ 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; +use function React\Async\await; +use function React\Promise\Stream\first; +use function React\Promise\Timer\timeout; class FunctionalIntegrationTest extends TestCase { @@ -49,12 +50,12 @@ public function testRequestToLocalhostEmitsSingleRemoteConnection() $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.0')); + $request = $client->request(new Request('GET', 'http://localhost/' . $port, [], '', '1.0')); - $promise = Stream\first($request, 'close'); + $promise = first($request, 'close'); $request->end(); - \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); + await(timeout($promise, self::TIMEOUT_LOCAL)); } public function testRequestToLocalhostWillConnectAndCloseConnectionAfterResponseWhenKeepAliveTimesOut() @@ -74,11 +75,11 @@ public function testRequestToLocalhostWillConnectAndCloseConnectionAfterResponse $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')); + $request = $client->request(new Request('GET', 'http://localhost/' . $port, [], '', '1.1')); $request->end(); - \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); + await(timeout($promise, self::TIMEOUT_LOCAL)); } public function testRequestToLocalhostWillReuseExistingConnectionForSecondRequest() @@ -96,17 +97,17 @@ public function testRequestToLocalhostWillReuseExistingConnectionForSecondReques $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 = $client->request(new Request('GET', 'http://localhost/' . $port, [], '', '1.1')); + $promise = first($request, 'close'); $request->end(); - \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); + await(timeout($promise, self::TIMEOUT_LOCAL)); - $request = $client->request(new Request('GET', 'http://localhost/' . $port, array(), '', '1.1')); - $promise = Stream\first($request, 'close'); + $request = $client->request(new Request('GET', 'http://localhost/' . $port, [], '', '1.1')); + $promise = first($request, 'close'); $request->end(); - \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); + await(timeout($promise, self::TIMEOUT_LOCAL)); } public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResponse() @@ -118,17 +119,17 @@ public function testRequestLegacyHttpServerWithOnlyLineFeedReturnsSuccessfulResp }); $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); - $request = $client->request(new Request('GET', str_replace('tcp:', 'http:', $socket->getAddress()), array(), '', '1.0')); + $request = $client->request(new Request('GET', str_replace('tcp:', 'http:', $socket->getAddress()), [], '', '1.0')); $once = $this->expectCallableOnceWith('body'); $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) { $body->on('data', $once); }); - $promise = Stream\first($request, 'close'); + $promise = first($request, 'close'); $request->end(); - \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_LOCAL)); + await(timeout($promise, self::TIMEOUT_LOCAL)); } /** @group internet */ @@ -136,17 +137,17 @@ public function testSuccessfulResponseEmitsEnd() { $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); - $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); + $request = $client->request(new Request('GET', 'http://www.google.com/', [], '', '1.0')); $once = $this->expectCallableOnce(); $request->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($once) { $body->on('end', $once); }); - $promise = Stream\first($request, 'close'); + $promise = first($request, 'close'); $request->end(); - \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT_REMOTE)); + await(timeout($promise, self::TIMEOUT_REMOTE)); } /** @group internet */ @@ -154,7 +155,7 @@ public function testCancelPendingConnectionEmitsClose() { $client = new Client(new ClientConnectionManager(new Connector(), Loop::get())); - $request = $client->request(new Request('GET', 'http://www.google.com/', array(), '', '1.0')); + $request = $client->request(new Request('GET', 'http://www.google.com/', [], '', '1.0')); $request->on('error', $this->expectCallableNever()); $request->on('close', $this->expectCallableOnce()); $request->end(); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 6a235703..92c873d1 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -10,12 +10,16 @@ use React\Http\Message\Response; use React\Http\Message\ResponseException; use React\Http\Middleware\StreamingRequestMiddleware; +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\Stream\ThroughStream; +use function React\Async\await; +use function React\Promise\Stream\buffer; +use function React\Promise\Timer\timeout; class FunctionalBrowserTest extends TestCase { @@ -35,7 +39,7 @@ public function setUpBrowserAndServer() $http = new HttpServer(new StreamingRequestMiddleware(), function (ServerRequestInterface $request) { $path = $request->getUri()->getPath(); - $headers = array(); + $headers = []; foreach ($request->getHeaders() as $name => $values) { $headers[$name] = implode(', ', $values); } @@ -43,7 +47,7 @@ public function setUpBrowserAndServer() if ($path === '/get') { return new Response( 200, - array(), + [], 'hello' ); } @@ -52,14 +56,14 @@ public function setUpBrowserAndServer() $params = $request->getQueryParams(); return new Response( 302, - array('Location' => $params['url']) + ['Location' => $params['url']] ); } if ($path === '/basic-auth/user/pass') { return new Response( $request->getHeaderLine('Authorization') === 'Basic dXNlcjpwYXNz' ? 200 : 401, - array(), + [], '' ); } @@ -67,7 +71,7 @@ public function setUpBrowserAndServer() if ($path === '/status/204') { return new Response( 204, - array(), + [], '' ); } @@ -75,7 +79,7 @@ public function setUpBrowserAndServer() if ($path === '/status/304') { return new Response( 304, - array(), + [], 'Not modified' ); } @@ -83,7 +87,7 @@ public function setUpBrowserAndServer() if ($path === '/status/404') { return new Response( 404, - array(), + [], '' ); } @@ -94,7 +98,7 @@ public function setUpBrowserAndServer() $timer = Loop::addTimer(10, function () use ($resolve) { $resolve(new Response( 200, - array(), + [], 'hello' )); }); @@ -116,11 +120,11 @@ public function setUpBrowserAndServer() $body->on('close', function () use (&$buffer, $resolve, $headers) { $resolve(new Response( 200, - array(), - json_encode(array( + [], + json_encode([ 'data' => $buffer, 'headers' => $headers - )) + ]) )); }); }); @@ -130,14 +134,14 @@ public function setUpBrowserAndServer() $stream = new ThroughStream(); Loop::futureTick(function () use ($stream, $headers) { - $stream->end(json_encode(array( + $stream->end(json_encode([ 'headers' => $headers - ))); + ])); }); return new Response( 200, - array(), + [], $stream ); } @@ -165,7 +169,7 @@ public function cleanUpSocketServer() */ public function testSimpleRequest() { - \React\Async\await($this->browser->get($this->base . 'get')); + await($this->browser->get($this->base . 'get')); } public function testGetRequestWithRelativeAddressRejects() @@ -173,7 +177,7 @@ public function testGetRequestWithRelativeAddressRejects() $promise = $this->browser->get('delay'); $this->setExpectedException('InvalidArgumentException', 'Invalid request URL given'); - \React\Async\await($promise); + await($promise); } /** @@ -181,7 +185,7 @@ public function testGetRequestWithRelativeAddressRejects() */ public function testGetRequestWithBaseAndRelativeAddressResolves() { - \React\Async\await($this->browser->withBase($this->base)->get('get')); + await($this->browser->withBase($this->base)->get('get')); } /** @@ -189,7 +193,7 @@ public function testGetRequestWithBaseAndRelativeAddressResolves() */ public function testGetRequestWithBaseAndFullAddressResolves() { - \React\Async\await($this->browser->withBase('http://example.com/')->get($this->base . 'get')); + await($this->browser->withBase('http://example.com/')->get($this->base . 'get')); } public function testCancelGetRequestWillRejectRequest() @@ -198,7 +202,7 @@ public function testCancelGetRequestWillRejectRequest() $promise->cancel(); $this->setExpectedException('RuntimeException'); - \React\Async\await($promise); + await($promise); } public function testCancelRequestWithPromiseFollowerWillRejectRequest() @@ -209,13 +213,13 @@ public function testCancelRequestWithPromiseFollowerWillRejectRequest() $promise->cancel(); $this->setExpectedException('RuntimeException'); - \React\Async\await($promise); + await($promise); } public function testRequestWithoutAuthenticationFails() { $this->setExpectedException('RuntimeException'); - \React\Async\await($this->browser->get($this->base . 'basic-auth/user/pass')); + await($this->browser->get($this->base . 'basic-auth/user/pass')); } /** @@ -225,7 +229,7 @@ public function testRequestWithAuthenticationSucceeds() { $base = str_replace('://', '://user:pass@', $this->base); - \React\Async\await($this->browser->get($base . 'basic-auth/user/pass')); + await($this->browser->get($base . 'basic-auth/user/pass')); } /** @@ -239,7 +243,7 @@ public function testRedirectToPageWithAuthenticationSendsAuthenticationFromLocat { $target = str_replace('://', '://user:pass@', $this->base) . 'basic-auth/user/pass'; - \React\Async\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($target))); + await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($target))); } /** @@ -254,7 +258,7 @@ public function testRedirectFromPageWithInvalidAuthToPageWithCorrectAuthenticati $base = str_replace('://', '://unknown:invalid@', $this->base); $target = str_replace('://', '://user:pass@', $this->base) . 'basic-auth/user/pass'; - \React\Async\await($this->browser->get($base . 'redirect-to?url=' . urlencode($target))); + await($this->browser->get($base . 'redirect-to?url=' . urlencode($target))); } public function testCancelRedirectedRequestShouldReject() @@ -266,7 +270,7 @@ public function testCancelRedirectedRequestShouldReject() }); $this->setExpectedException('RuntimeException', 'Request cancelled'); - \React\Async\await($promise); + await($promise); } public function testTimeoutDelayedResponseShouldReject() @@ -274,17 +278,17 @@ public function testTimeoutDelayedResponseShouldReject() $promise = $this->browser->withTimeout(0.1)->get($this->base . 'delay/10'); $this->setExpectedException('RuntimeException', 'Request timed out after 0.1 seconds'); - \React\Async\await($promise); + await($promise); } public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject() { $stream = new ThroughStream(); - $promise = $this->browser->withTimeout(0.1)->post($this->base . 'delay/10', array(), $stream); + $promise = $this->browser->withTimeout(0.1)->post($this->base . 'delay/10', [], $stream); $stream->end(); $this->setExpectedException('RuntimeException', 'Request timed out after 0.1 seconds'); - \React\Async\await($promise); + await($promise); } /** @@ -292,7 +296,7 @@ public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject() */ public function testTimeoutFalseShouldResolveSuccessfully() { - \React\Async\await($this->browser->withTimeout(false)->get($this->base . 'get')); + await($this->browser->withTimeout(false)->get($this->base . 'get')); } /** @@ -300,7 +304,7 @@ public function testTimeoutFalseShouldResolveSuccessfully() */ public function testRedirectRequestRelative() { - \React\Async\await($this->browser->get($this->base . 'redirect-to?url=get')); + await($this->browser->get($this->base . 'redirect-to?url=get')); } /** @@ -308,7 +312,7 @@ public function testRedirectRequestRelative() */ public function testRedirectRequestAbsolute() { - \React\Async\await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($this->base . 'get'))); + await($this->browser->get($this->base . 'redirect-to?url=' . urlencode($this->base . 'get'))); } /** @@ -318,7 +322,7 @@ public function testFollowingRedirectsFalseResolvesWithRedirectResult() { $browser = $this->browser->withFollowRedirects(false); - \React\Async\await($browser->get($this->base . 'redirect-to?url=get')); + await($browser->get($this->base . 'redirect-to?url=get')); } public function testFollowRedirectsZeroRejectsOnRedirect() @@ -326,12 +330,12 @@ public function testFollowRedirectsZeroRejectsOnRedirect() $browser = $this->browser->withFollowRedirects(0); $this->setExpectedException('RuntimeException'); - \React\Async\await($browser->get($this->base . 'redirect-to?url=get')); + await($browser->get($this->base . 'redirect-to?url=get')); } public function testResponseStatus204ShouldResolveWithEmptyBody() { - $response = \React\Async\await($this->browser->get($this->base . 'status/204')); + $response = await($this->browser->get($this->base . 'status/204')); $this->assertFalse($response->hasHeader('Content-Length')); $body = $response->getBody(); @@ -341,7 +345,7 @@ public function testResponseStatus204ShouldResolveWithEmptyBody() public function testResponseStatus304ShouldResolveWithEmptyBodyButContentLengthResponseHeader() { - $response = \React\Async\await($this->browser->get($this->base . 'status/304')); + $response = await($this->browser->get($this->base . 'status/304')); $this->assertEquals('12', $response->getHeaderLine('Content-Length')); $body = $response->getBody(); @@ -356,7 +360,7 @@ public function testGetRequestWithResponseBufferMatchedExactlyResolves() { $promise = $this->browser->withResponseBuffer(5)->get($this->base . 'get'); - \React\Async\await($promise); + await($promise); } public function testGetRequestWithResponseBufferExceededRejects() @@ -368,7 +372,7 @@ public function testGetRequestWithResponseBufferExceededRejects() 'Response body size of 5 bytes exceeds maximum of 4 bytes', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 ); - \React\Async\await($promise); + await($promise); } public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() @@ -380,7 +384,7 @@ public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() 'Response body size exceeds maximum of 4 bytes', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 ); - \React\Async\await($promise); + await($promise); } /** @@ -389,7 +393,7 @@ public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() */ public function testCanAccessHttps() { - \React\Async\await($this->browser->get('https://www.google.com/')); + await($this->browser->get('https://www.google.com/')); } /** @@ -397,16 +401,16 @@ public function testCanAccessHttps() */ public function testVerifyPeerEnabledForBadSslRejects() { - $connector = new Connector(array( - 'tls' => array( + $connector = new Connector([ + 'tls' => [ 'verify_peer' => true - ) - )); + ] + ]); $browser = new Browser($connector); $this->setExpectedException('RuntimeException'); - \React\Async\await($browser->get('https://self-signed.badssl.com/')); + await($browser->get('https://self-signed.badssl.com/')); } /** @@ -415,15 +419,15 @@ public function testVerifyPeerEnabledForBadSslRejects() */ public function testVerifyPeerDisabledForBadSslResolves() { - $connector = new Connector(array( - 'tls' => array( + $connector = new Connector([ + 'tls' => [ 'verify_peer' => false - ) - )); + ] + ]); $browser = new Browser($connector); - \React\Async\await($browser->get('https://self-signed.badssl.com/')); + await($browser->get('https://self-signed.badssl.com/')); } /** @@ -432,13 +436,13 @@ public function testVerifyPeerDisabledForBadSslResolves() public function testInvalidPort() { $this->setExpectedException('RuntimeException'); - \React\Async\await($this->browser->get('http://www.google.com:443/')); + await($this->browser->get('http://www.google.com:443/')); } public function testErrorStatusCodeRejectsWithResponseException() { try { - \React\Async\await($this->browser->get($this->base . 'status/404')); + await($this->browser->get($this->base . 'status/404')); $this->fail(); } catch (ResponseException $e) { $this->assertEquals(404, $e->getCode()); @@ -450,14 +454,14 @@ public function testErrorStatusCodeRejectsWithResponseException() public function testErrorStatusCodeDoesNotRejectWithRejectErrorResponseFalse() { - $response = \React\Async\await($this->browser->withRejectErrorResponse(false)->get($this->base . 'status/404')); + $response = await($this->browser->withRejectErrorResponse(false)->get($this->base . 'status/404')); $this->assertEquals(404, $response->getStatusCode()); } public function testPostString() { - $response = \React\Async\await($this->browser->post($this->base . 'post', array(), 'hello world')); + $response = await($this->browser->post($this->base . 'post', [], 'hello world')); $data = json_decode((string)$response->getBody(), true); $this->assertEquals('hello world', $data['data']); @@ -465,7 +469,7 @@ public function testPostString() public function testRequestStreamReturnsResponseBodyUntilConnectionsEndsForHttp10() { - $response = \React\Async\await($this->browser->withProtocolVersion('1.0')->get($this->base . 'stream/1')); + $response = await($this->browser->withProtocolVersion('1.0')->get($this->base . 'stream/1')); $this->assertEquals('1.0', $response->getProtocolVersion()); $this->assertFalse($response->hasHeader('Transfer-Encoding')); @@ -476,7 +480,7 @@ public function testRequestStreamReturnsResponseBodyUntilConnectionsEndsForHttp1 public function testRequestStreamReturnsResponseWithTransferEncodingChunkedAndResponseBodyDecodedForHttp11() { - $response = \React\Async\await($this->browser->get($this->base . 'stream/1')); + $response = await($this->browser->get($this->base . 'stream/1')); $this->assertEquals('1.1', $response->getProtocolVersion()); @@ -488,7 +492,7 @@ public function testRequestStreamReturnsResponseWithTransferEncodingChunkedAndRe public function testRequestStreamWithHeadRequestReturnsEmptyResponseBodWithTransferEncodingChunkedForHttp11() { - $response = \React\Async\await($this->browser->head($this->base . 'stream/1')); + $response = await($this->browser->head($this->base . 'stream/1')); $this->assertEquals('1.1', $response->getProtocolVersion()); @@ -499,7 +503,7 @@ public function testRequestStreamWithHeadRequestReturnsEmptyResponseBodWithTrans public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenResponseHasDoubleTransferEncoding() { $socket = new SocketServer('127.0.0.1:0'); - $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) { + $socket->on('connection', function (ConnectionInterface $connection) { $connection->on('data', function () use ($connection) { $connection->end("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked, chunked\r\nConnection: close\r\n\r\nhello"); }); @@ -507,7 +511,7 @@ public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenRes $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = \React\Async\await($this->browser->get($this->base . 'stream/1')); + $response = await($this->browser->get($this->base . 'stream/1')); $socket->close(); @@ -519,9 +523,9 @@ public function testRequestStreamReturnsResponseWithResponseBodyUndecodedWhenRes public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeepsConnectionOpen() { - $closed = new \React\Promise\Deferred(); + $closed = new Deferred(); $socket = new SocketServer('127.0.0.1:0'); - $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) use ($closed) { + $socket->on('connection', function (ConnectionInterface $connection) use ($closed) { $connection->on('data', function () use ($connection) { $connection->write("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"); }); @@ -532,10 +536,10 @@ public function testReceiveStreamAndExplicitlyCloseConnectionEvenWhenServerKeeps $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = \React\Async\await($this->browser->get($this->base . 'get', array())); + $response = await($this->browser->get($this->base . 'get', [])); $this->assertEquals('hello', (string)$response->getBody()); - $ret = \React\Async\await(\React\Promise\Timer\timeout($closed->promise(), 0.1)); + $ret = await(timeout($closed->promise(), 0.1)); $this->assertTrue($ret); $socket->close(); @@ -545,7 +549,7 @@ public function testRequestWithConnectionCloseHeaderWillCreateNewConnectionForSe { $twice = $this->expectCallableOnce(); $socket = new SocketServer('127.0.0.1:0'); - $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) use ($socket, $twice) { + $socket->on('connection', function (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"); }); @@ -561,11 +565,11 @@ public function testRequestWithConnectionCloseHeaderWillCreateNewConnectionForSe // 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')); + $response = 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')); + $response = await($this->browser->get($this->base . 'get')); assert($response instanceof ResponseInterface); $this->assertEquals('hello', (string)$response->getBody()); } @@ -574,7 +578,7 @@ public function testRequestWithHttp10WillCreateNewConnectionForSecondRequestEven { $twice = $this->expectCallableOnce(); $socket = new SocketServer('127.0.0.1:0'); - $socket->on('connection', function (\React\Socket\ConnectionInterface $connection) use ($socket, $twice) { + $socket->on('connection', function (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"); }); @@ -590,11 +594,11 @@ public function testRequestWithHttp10WillCreateNewConnectionForSecondRequestEven // 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')); + $response = 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')); + $response = await($this->browser->get($this->base . 'get')); assert($response instanceof ResponseInterface); $this->assertEquals('hello', (string)$response->getBody()); } @@ -603,11 +607,11 @@ public function testRequestWillReuseExistingConnectionForSecondRequestByDefault( { $this->socket->on('connection', $this->expectCallableOnce()); - $response = \React\Async\await($this->browser->get($this->base . 'get')); + $response = 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')); + $response = await($this->browser->get($this->base . 'get')); assert($response instanceof ResponseInterface); $this->assertEquals('hello', (string)$response->getBody()); } @@ -619,11 +623,11 @@ public function testRequestWithHttp10AndConnectionKeepAliveHeaderWillReuseExisti $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')); + $response = 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')); + $response = await($this->browser->get($this->base . 'get')); assert($response instanceof ResponseInterface); $this->assertEquals('hello', (string)$response->getBody()); } @@ -635,7 +639,7 @@ public function testRequestWithoutConnectionHeaderWillReuseExistingConnectionFor // 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')); + $response = await($this->browser->get($this->base . 'redirect-to?url=get')); assert($response instanceof ResponseInterface); $this->assertEquals('hello', (string)$response->getBody()); } @@ -648,7 +652,7 @@ public function testPostStreamChunked() $stream->end('hello world'); }); - $response = \React\Async\await($this->browser->post($this->base . 'post', array(), $stream)); + $response = await($this->browser->post($this->base . 'post', [], $stream)); $data = json_decode((string)$response->getBody(), true); $this->assertEquals('hello world', $data['data']); @@ -664,7 +668,7 @@ public function testPostStreamKnownLength() $stream->end('hello world'); }); - $response = \React\Async\await($this->browser->post($this->base . 'post', array('Content-Length' => 11), $stream)); + $response = await($this->browser->post($this->base . 'post', ['Content-Length' => 11], $stream)); $data = json_decode((string)$response->getBody(), true); $this->assertEquals('hello world', $data['data']); @@ -684,7 +688,7 @@ public function testPostStreamWillStartSendingRequestEvenWhenBodyDoesNotEmitData $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; $stream = new ThroughStream(); - \React\Async\await($this->browser->post($this->base . 'post', array(), $stream)); + await($this->browser->post($this->base . 'post', [], $stream)); $socket->close(); } @@ -694,7 +698,7 @@ public function testPostStreamClosed() $stream = new ThroughStream(); $stream->close(); - $response = \React\Async\await($this->browser->post($this->base . 'post', array(), $stream)); + $response = await($this->browser->post($this->base . 'post', [], $stream)); $data = json_decode((string)$response->getBody(), true); $this->assertEquals('', $data['data']); @@ -705,7 +709,7 @@ public function testSendsHttp11ByDefault() $http = new HttpServer(function (ServerRequestInterface $request) { return new Response( 200, - array(), + [], $request->getProtocolVersion() ); }); @@ -714,7 +718,7 @@ public function testSendsHttp11ByDefault() $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = \React\Async\await($this->browser->get($this->base)); + $response = await($this->browser->get($this->base)); $this->assertEquals('1.1', (string)$response->getBody()); $socket->close(); @@ -725,7 +729,7 @@ public function testSendsExplicitHttp10Request() $http = new HttpServer(function (ServerRequestInterface $request) { return new Response( 200, - array(), + [], $request->getProtocolVersion() ); }); @@ -734,7 +738,7 @@ public function testSendsExplicitHttp10Request() $this->base = str_replace('tcp:', 'http:', $socket->getAddress()) . '/'; - $response = \React\Async\await($this->browser->withProtocolVersion('1.0')->get($this->base)); + $response = await($this->browser->withProtocolVersion('1.0')->get($this->base)); $this->assertEquals('1.0', (string)$response->getBody()); $socket->close(); @@ -742,7 +746,7 @@ public function testSendsExplicitHttp10Request() public function testHeadRequestReceivesResponseWithEmptyBodyButWithContentLengthResponseHeader() { - $response = \React\Async\await($this->browser->head($this->base . 'get')); + $response = await($this->browser->head($this->base . 'get')); $this->assertEquals('5', $response->getHeaderLine('Content-Length')); $body = $response->getBody(); @@ -752,7 +756,7 @@ public function testHeadRequestReceivesResponseWithEmptyBodyButWithContentLength public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndKnownSize() { - $response = \React\Async\await($this->browser->requestStreaming('GET', $this->base . 'get')); + $response = await($this->browser->requestStreaming('GET', $this->base . 'get')); $this->assertEquals('5', $response->getHeaderLine('Content-Length')); $body = $response->getBody(); @@ -763,7 +767,7 @@ public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndKnown public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndUnknownSizeFromStreamingEndpoint() { - $response = \React\Async\await($this->browser->requestStreaming('GET', $this->base . 'stream/1')); + $response = await($this->browser->requestStreaming('GET', $this->base . 'stream/1')); $this->assertFalse($response->hasHeader('Content-Length')); $body = $response->getBody(); @@ -774,9 +778,9 @@ public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndUnkno public function testRequestStreamingGetReceivesStreamingResponseBody() { - $buffer = \React\Async\await( + $buffer = await( $this->browser->requestStreaming('GET', $this->base . 'get')->then(function (ResponseInterface $response) { - return Stream\buffer($response->getBody()); + return buffer($response->getBody()); }) ); @@ -785,9 +789,9 @@ public function testRequestStreamingGetReceivesStreamingResponseBody() public function testRequestStreamingGetReceivesStreamingResponseBodyEvenWhenResponseBufferExceeded() { - $buffer = \React\Async\await( + $buffer = await( $this->browser->withResponseBuffer(4)->requestStreaming('GET', $this->base . 'get')->then(function (ResponseInterface $response) { - return Stream\buffer($response->getBody()); + return buffer($response->getBody()); }) ); diff --git a/tests/FunctionalHttpServerTest.php b/tests/FunctionalHttpServerTest.php index 6b153b81..c0fcfe83 100644 --- a/tests/FunctionalHttpServerTest.php +++ b/tests/FunctionalHttpServerTest.php @@ -10,12 +10,17 @@ use React\Http\Middleware\LimitConcurrentRequestsMiddleware; use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\StreamingRequestMiddleware; +use React\Promise\Promise; use React\Socket\ConnectionInterface; use React\Socket\Connector; use React\Socket\SocketServer; -use React\Promise; -use React\Promise\Stream; use React\Stream\ThroughStream; +use function React\Async\await; +use function React\Promise\all; +use function React\Promise\Stream\buffer; +use function React\Promise\Stream\first; +use function React\Promise\Timer\sleep; +use function React\Promise\Timer\timeout; class FunctionalHttpServerTest extends TestCase { @@ -24,7 +29,7 @@ public function testPlainHttpOnRandomPort() $connector = new Connector(); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); $socket = new SocketServer('127.0.0.1:0'); @@ -33,10 +38,10 @@ public function testPlainHttpOnRandomPort() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); @@ -60,10 +65,10 @@ function () { $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 404 Not Found", $response); @@ -75,7 +80,7 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() $connector = new Connector(); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); $socket = new SocketServer('127.0.0.1:0'); @@ -84,10 +89,10 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); @@ -100,7 +105,7 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() $connector = new Connector(); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); $socket = new SocketServer('127.0.0.1:0'); @@ -109,10 +114,10 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: localhost:1000\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://localhost:1000/', $response); @@ -122,26 +127,28 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() public function testSecureHttpsOnRandomPort() { - $connector = new Connector(array( - 'tls' => array('verify_peer' => false) - )); + $connector = new Connector([ + 'tls' => [ + 'verify_peer' => false + ] + ]); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); - $socket = new SocketServer('tls://127.0.0.1:0', array('tls' => array( + $socket = new SocketServer('tls://127.0.0.1:0', ['tls' => [ 'local_cert' => __DIR__ . '/../examples/localhost.pem' - ))); + ]]); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); @@ -154,27 +161,25 @@ public function testSecureHttpsReturnsData() $http = new HttpServer(function (RequestInterface $request) { return new Response( 200, - array(), + [], str_repeat('.', 33000) ); }); - $socket = new SocketServer('tls://127.0.0.1:0', array('tls' => array( - 'local_cert' => __DIR__ . '/../examples/localhost.pem' - ))); + $socket = new SocketServer('tls://127.0.0.1:0', ['tls' => ['local_cert' => __DIR__ . '/../examples/localhost.pem']]); $http->listen($socket); - $connector = new Connector(array( - 'tls' => array('verify_peer' => false) - )); + $connector = new Connector(['tls' => [ + 'verify_peer' => false + ]]); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString("\r\nContent-Length: 33000\r\n", $response); @@ -185,26 +190,26 @@ public function testSecureHttpsReturnsData() public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() { - $connector = new Connector(array( - 'tls' => array('verify_peer' => false) - )); + $connector = new Connector([ + 'tls' => ['verify_peer' => false] + ]); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); - $socket = new SocketServer('tls://127.0.0.1:0', array('tls' => array( + $socket = new SocketServer('tls://127.0.0.1:0', ['tls' => [ 'local_cert' => __DIR__ . '/../examples/localhost.pem' - ))); + ]]); $http->listen($socket); $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); @@ -222,7 +227,7 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() $connector = new Connector(); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); $http->listen($socket); @@ -230,10 +235,10 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://127.0.0.1/', $response); @@ -251,7 +256,7 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort $connector = new Connector(); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); $http->listen($socket); @@ -259,10 +264,10 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://127.0.0.1/', $response); @@ -273,19 +278,19 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() { try { - $socket = new SocketServer('tls://127.0.0.1:443', array('tls' => array( + $socket = new SocketServer('tls://127.0.0.1:443', ['tls' => [ 'local_cert' => __DIR__ . '/../examples/localhost.pem' - ))); + ]]); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 443 failed (root and unused?)'); } - $connector = new Connector(array( - 'tls' => array('verify_peer' => false) - )); + $connector = new Connector([ + 'tls' => ['verify_peer' => false] + ]); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); $http->listen($socket); @@ -293,10 +298,10 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://127.0.0.1/', $response); @@ -307,19 +312,19 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() { try { - $socket = new SocketServer('tls://127.0.0.1:443', array('tls' => array( + $socket = new SocketServer('tls://127.0.0.1:443', ['tls' => [ 'local_cert' => __DIR__ . '/../examples/localhost.pem' - ))); + ]]); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 443 failed (root and unused?)'); } - $connector = new Connector(array( - 'tls' => array('verify_peer' => false) - )); + $connector = new Connector([ + 'tls' => ['verify_peer' => false] + ]); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); $http->listen($socket); @@ -327,10 +332,10 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://127.0.0.1/', $response); @@ -348,7 +353,7 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() $connector = new Connector(); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri()); + return new Response(200, [], (string)$request->getUri()); }); $http->listen($socket); @@ -356,10 +361,10 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('http://127.0.0.1:443/', $response); @@ -370,19 +375,19 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() { try { - $socket = new SocketServer('tls://127.0.0.1:80', array('tls' => array( + $socket = new SocketServer('tls://127.0.0.1:80', ['tls' => [ 'local_cert' => __DIR__ . '/../examples/localhost.pem' - ))); + ]]); } catch (\RuntimeException $e) { $this->markTestSkipped('Listening on port 80 failed (root and unused?)'); } - $connector = new Connector(array( - 'tls' => array('verify_peer' => false) - )); + $connector = new Connector([ + 'tls' => ['verify_peer' => false] + ]); $http = new HttpServer(function (RequestInterface $request) { - return new Response(200, array(), (string)$request->getUri() . 'x' . $request->getHeaderLine('Host')); + return new Response(200, [], (string)$request->getUri() . 'x' . $request->getHeaderLine('Host')); }); $http->listen($socket); @@ -390,10 +395,10 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\nHost: " . noScheme($conn->getRemoteAddress()) . "\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertContainsString("HTTP/1.0 200 OK", $response); $this->assertContainsString('https://127.0.0.1:80/', $response); @@ -409,7 +414,7 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() $stream->close(); $http = new HttpServer(function (RequestInterface $request) use ($stream) { - return new Response(200, array(), $stream); + return new Response(200, [], $stream); }); $socket = new SocketServer('127.0.0.1:0'); @@ -418,10 +423,10 @@ public function testClosedStreamFromRequestHandlerWillSendEmptyBody() $result = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write("GET / HTTP/1.0\r\n\r\n"); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.0 200 OK", $response); $this->assertStringEndsWith("\r\n\r\n", $response); @@ -452,7 +457,7 @@ function (RequestInterface $request) use ($once) { }); }); - \React\Async\await(\React\Promise\Timer\sleep(0.1)); + await(sleep(0.1)); $socket->close(); } @@ -466,7 +471,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionClosesWhileS $http = new HttpServer( new StreamingRequestMiddleware(), function (RequestInterface $request) use ($stream) { - return new Response(200, array(), $stream); + return new Response(200, [], $stream); } ); @@ -482,7 +487,7 @@ function (RequestInterface $request) use ($stream) { }); // stream will be closed within 0.1s - $ret = \React\Async\await(\React\Promise\Timer\timeout(Stream\first($stream, 'close'), 0.1)); + $ret = await(timeout(first($stream, 'close'), 0.1)); $socket->close(); @@ -496,7 +501,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() $stream = new ThroughStream(); $http = new HttpServer(function (RequestInterface $request) use ($stream) { - return new Response(200, array(), $stream); + return new Response(200, [], $stream); }); $socket = new SocketServer('127.0.0.1:0'); @@ -511,7 +516,7 @@ public function testStreamFromRequestHandlerWillBeClosedIfConnectionCloses() }); // await response stream to be closed - $ret = \React\Async\await(\React\Promise\Timer\timeout(Stream\first($stream, 'close'), 1.0)); + $ret = await(timeout(first($stream, 'close'), 1.0)); $socket->close(); @@ -529,7 +534,7 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() $stream->end(); }); - return new Response(101, array('Upgrade' => 'echo'), $stream); + return new Response(101, ['Upgrade' => 'echo'], $stream); }); $socket = new SocketServer('127.0.0.1:0'); @@ -543,10 +548,10 @@ public function testUpgradeWithThroughStreamReturnsDataAsGiven() $conn->write('world'); }); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 101 Switching Protocols\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -565,7 +570,7 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() $stream->end(); }); - return new Response(101, array('Upgrade' => 'echo'), $stream); + return new Response(101, ['Upgrade' => 'echo'], $stream); }); $socket = new SocketServer('127.0.0.1:0'); @@ -580,10 +585,10 @@ public function testUpgradeWithRequestBodyAndThroughStreamReturnsDataAsGiven() $conn->write('world'); }); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 101 Switching Protocols\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -602,7 +607,7 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() $stream->end(); }); - return new Response(200, array(), $stream); + return new Response(200, [], $stream); }); $socket = new SocketServer('127.0.0.1:0'); @@ -616,10 +621,10 @@ public function testConnectWithThroughStreamReturnsDataAsGiven() $conn->write('world'); }); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -638,9 +643,9 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive $stream->end(); }); - return new Promise\Promise(function ($resolve) use ($stream) { + return new Promise(function ($resolve) use ($stream) { Loop::addTimer(0.001, function () use ($resolve, $stream) { - $resolve(new Response(200, array(), $stream)); + $resolve(new Response(200, [], $stream)); }); }); }); @@ -656,10 +661,10 @@ public function testConnectWithThroughStreamReturnedFromPromiseReturnsDataAsGive $conn->write('world'); }); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); $this->assertStringEndsWith("\r\n\r\nhelloworld", $response); @@ -675,7 +680,7 @@ public function testConnectWithClosedThroughStreamReturnsNoData() $stream = new ThroughStream(); $stream->close(); - return new Response(200, array(), $stream); + return new Response(200, [], $stream); }); $socket = new SocketServer('127.0.0.1:0'); @@ -689,10 +694,10 @@ public function testConnectWithClosedThroughStreamReturnsNoData() $conn->write('world'); }); - return Stream\buffer($conn); + return buffer($conn); }); - $response = \React\Async\await(\React\Promise\Timer\timeout($result, 1.0)); + $response = await(timeout($result, 1.0)); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response); $this->assertStringEndsWith("\r\n\r\n", $response); @@ -708,21 +713,21 @@ public function testLimitConcurrentRequestsMiddlewareRequestStreamPausing() new LimitConcurrentRequestsMiddleware(5), new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (ServerRequestInterface $request, $next) { - return new Promise\Promise(function ($resolve) use ($request, $next) { + return new Promise(function ($resolve) use ($request, $next) { Loop::addTimer(0.1, function () use ($request, $resolve, $next) { $resolve($next($request)); }); }); }, function (ServerRequestInterface $request) { - return new Response(200, array(), (string)strlen((string)$request->getBody())); + return new Response(200, [], (string)strlen((string)$request->getBody())); } ); $socket = new SocketServer('127.0.0.1:0'); $http->listen($socket); - $result = array(); + $result = []; for ($i = 0; $i < 6; $i++) { $result[] = $connector->connect($socket->getAddress())->then(function (ConnectionInterface $conn) { $conn->write( @@ -731,11 +736,11 @@ function (ServerRequestInterface $request) { "\r\n\r\n" ); - return Stream\buffer($conn); + return buffer($conn); }); } - $responses = \React\Async\await(\React\Promise\Timer\timeout(Promise\all($result), 1.0)); + $responses = await(timeout(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 606c50a6..fc977d9b 100644 --- a/tests/HttpServerTest.php +++ b/tests/HttpServerTest.php @@ -7,9 +7,10 @@ use React\Http\HttpServer; use React\Http\Io\IniUtil; use React\Http\Middleware\StreamingRequestMiddleware; -use React\Promise; use React\Promise\Deferred; use React\Stream\ReadableStreamInterface; +use function React\Async\await; +use function React\Promise\reject; final class HttpServerTest extends TestCase { @@ -27,7 +28,7 @@ public function setUpConnectionMockAndSocket() $this->connection = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() ->setMethods( - array( + [ 'write', 'end', 'close', @@ -38,7 +39,7 @@ public function setUpConnectionMockAndSocket() 'getRemoteAddress', 'getLocalAddress', 'pipe' - ) + ] ) ->getMock(); @@ -81,8 +82,8 @@ public function testSimpleRequestCallsRequestHandlerOnce() }); $http->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); + $this->socket->emit('connection', [$this->connection]); + $this->connection->emit('data', ["GET / HTTP/1.0\r\n\r\n"]); $this->assertSame(1, $called); } @@ -90,11 +91,11 @@ public function testSimpleRequestCallsRequestHandlerOnce() public function testSimpleRequestCallsArrayRequestHandlerOnce() { $this->called = null; - $http = new HttpServer(array($this, 'helperCallableOnce')); + $http = new HttpServer([$this, 'helperCallableOnce']); $http->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); + $this->socket->emit('connection', [$this->connection]); + $this->connection->emit('data', ["GET / HTTP/1.0\r\n\r\n"]); $this->assertSame(1, $this->called); } @@ -121,8 +122,8 @@ function (ServerRequestInterface $request) use (&$called) { ); $http->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); + $this->socket->emit('connection', [$this->connection]); + $this->connection->emit('data', ["GET / HTTP/1.0\r\n\r\n"]); $this->assertSame('beforeokafter', $called); } @@ -135,10 +136,10 @@ public function testPostFormData() }); $http->listen($this->socket); - $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")); + $this->socket->emit('connection', [$this->connection]); + $this->connection->emit('data', ["POST / HTTP/1.0\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 7\r\n\r\nfoo=bar"]); - $request = \React\Async\await($deferred->promise()); + $request = await($deferred->promise()); assert($request instanceof ServerRequestInterface); $form = $request->getParsedBody(); @@ -146,7 +147,7 @@ public function testPostFormData() $this->assertTrue(isset($form['foo'])); $this->assertEquals('bar', $form['foo']); - $this->assertEquals(array(), $request->getUploadedFiles()); + $this->assertEquals([], $request->getUploadedFiles()); $body = $request->getBody(); @@ -163,20 +164,19 @@ public function testPostFileUpload() }); $http->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); - $connection = $this->connection; $data = $this->createPostFileUploadRequest(); - Loop::addPeriodicTimer(0.01, function ($timer) use (&$data, $connection) { + Loop::addPeriodicTimer(0.01, function ($timer) use (&$data) { $line = array_shift($data); - $connection->emit('data', array($line)); + $this->connection->emit('data', [$line]); if (count($data) === 0) { Loop::cancelTimer($timer); } }); - $request = \React\Async\await($deferred->promise()); + $request = await($deferred->promise()); assert($request instanceof ServerRequestInterface); $this->assertEmpty($request->getParsedBody()); @@ -206,15 +206,15 @@ public function testPostJsonWillNotBeParsedByDefault() }); $http->listen($this->socket); - $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]")); + $this->socket->emit('connection', [$this->connection]); + $this->connection->emit('data', ["POST / HTTP/1.0\r\nContent-Type: application/json\r\nContent-Length: 6\r\n\r\n[true]"]); - $request = \React\Async\await($deferred->promise()); + $request = await($deferred->promise()); assert($request instanceof ServerRequestInterface); $this->assertNull($request->getParsedBody()); - $this->assertSame(array(), $request->getUploadedFiles()); + $this->assertSame([], $request->getUploadedFiles()); $body = $request->getBody(); @@ -231,8 +231,8 @@ public function testServerReceivesBufferedRequestByDefault() }); $http->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); + $this->socket->emit('connection', [$this->connection]); + $this->connection->emit('data', ["GET / HTTP/1.0\r\n\r\n"]); $this->assertEquals(false, $streaming); } @@ -248,8 +248,8 @@ function (ServerRequestInterface $request) use (&$streaming) { ); $http->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - $this->connection->emit('data', array("GET / HTTP/1.0\r\n\r\n")); + $this->socket->emit('connection', [$this->connection]); + $this->connection->emit('data', ["GET / HTTP/1.0\r\n\r\n"]); $this->assertEquals(true, $streaming); } @@ -259,17 +259,17 @@ public function testForwardErrors() $exception = new \Exception(); $capturedException = null; $http = new HttpServer(function () use ($exception) { - return Promise\reject($exception); + return reject($exception); }); $http->on('error', function ($error) use (&$capturedException) { $capturedException = $error; }); $http->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createPostFileUploadRequest(); - $this->connection->emit('data', array(implode('', $data))); + $this->connection->emit('data', [implode('', $data)]); $this->assertInstanceOf('RuntimeException', $capturedException); $this->assertInstanceOf('Exception', $capturedException->getPrevious()); @@ -280,7 +280,7 @@ private function createPostFileUploadRequest() { $boundary = "---------------------------5844729766471062541057622570"; - $data = array(); + $data = []; $data[] = "POST / HTTP/1.1\r\n"; $data[] = "Host: localhost\r\n"; $data[] = "Content-Type: multipart/form-data; boundary=" . $boundary . "\r\n"; @@ -299,23 +299,23 @@ private function createPostFileUploadRequest() public function provideIniSettingsForConcurrency() { - return array( - 'default settings' => array( + return [ + 'default settings' => [ '128M', '64K', // 8M capped at maximum 1024 - ), - 'unlimited memory_limit has no concurrency limit' => array( + ], + 'unlimited memory_limit has no concurrency limit' => [ '-1', '8M', null - ), - 'small post_max_size results in high concurrency' => array( + ], + 'small post_max_size results in high concurrency' => [ '128M', '1k', 65536 - ) - ); + ] + ]; } /** diff --git a/tests/Io/AbstractMessageTest.php b/tests/Io/AbstractMessageTest.php index 9e2c7d32..59c170ec 100644 --- a/tests/Io/AbstractMessageTest.php +++ b/tests/Io/AbstractMessageTest.php @@ -25,7 +25,7 @@ public function testWithProtocolVersionReturnsNewInstanceWhenProtocolVersionIsCh { $message = new MessageMock( '1.1', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() ); @@ -39,7 +39,7 @@ public function testWithProtocolVersionReturnsSameInstanceWhenProtocolVersionIsU { $message = new MessageMock( '1.1', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() ); @@ -52,16 +52,16 @@ 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(['Content-Type' => ['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->getHeader('Content-Type')); + $this->assertEquals(['text/plain'], $message->getHeader('CONTENT-type')); $this->assertEquals('text/plain', $message->getHeaderLine('Content-Type')); $this->assertEquals('text/plain', $message->getHeaderLine('CONTENT-Type')); @@ -72,50 +72,50 @@ public function testHeaderWithStringValue() $new = $message->withHeader('Content-Type', 'text/plain'); $this->assertSame($message, $new); - $new = $message->withHeader('Content-Type', array('text/plain')); + $new = $message->withHeader('Content-Type', ['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()); + $this->assertEquals(['content-type' => ['text/plain']], $new->getHeaders()); + $this->assertEquals(['Content-Type' => ['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()); + $this->assertEquals(['Content-Type' => ['text/html']], $new->getHeaders()); + $this->assertEquals(['Content-Type' => ['text/plain']], $message->getHeaders()); - $new = $message->withHeader('Content-Type', array('text/html')); + $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()); + $this->assertEquals(['Content-Type' => ['text/html']], $new->getHeaders()); + $this->assertEquals(['Content-Type' => ['text/plain']], $message->getHeaders()); - $new = $message->withAddedHeader('Content-Type', array()); + $new = $message->withAddedHeader('Content-Type', []); $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()); + $this->assertEquals([], $new->getHeaders()); + $this->assertEquals(['Content-Type' => ['text/plain']], $message->getHeaders()); } public function testHeaderWithMultipleValues() { $message = new MessageMock( '1.1', - array( - 'Set-Cookie' => array( + [ + 'Set-Cookie' => [ '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(['Set-Cookie' => ['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->getHeader('Set-Cookie')); + $this->assertEquals(['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')); @@ -123,49 +123,49 @@ public function testHeaderWithMultipleValues() $this->assertTrue($message->hasHeader('Set-Cookie')); $this->assertTrue($message->hasHeader('Set-Cookie')); - $new = $message->withHeader('Set-Cookie', array('a=1', 'b=2')); + $new = $message->withHeader('Set-Cookie', ['a=1', 'b=2']); $this->assertSame($message, $new); - $new = $message->withHeader('Set-Cookie', array('a=1', 'b=2', 'c=3')); + $new = $message->withHeader('Set-Cookie', ['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()); + $this->assertEquals(['Set-Cookie' => ['a=1', 'b=2', 'c=3']], $new->getHeaders()); + $this->assertEquals(['Set-Cookie' => ['a=1', 'b=2']], $message->getHeaders()); - $new = $message->withAddedHeader('Set-Cookie', array()); + $new = $message->withAddedHeader('Set-Cookie', []); $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()); + $this->assertEquals(['Set-Cookie' => ['a=1', 'b=2', 'c=3']], $new->getHeaders()); + $this->assertEquals(['Set-Cookie' => ['a=1', 'b=2']], $message->getHeaders()); - $new = $message->withAddedHeader('Set-Cookie', array('c=3')); + $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()); + $this->assertEquals(['Set-Cookie' => ['a=1', 'b=2', 'c=3']], $new->getHeaders()); + $this->assertEquals(['Set-Cookie' => ['a=1', 'b=2']], $message->getHeaders()); } public function testHeaderWithEmptyValue() { $message = new MessageMock( '1.1', - array( - 'Content-Type' => array() - ), + [ + 'Content-Type' => [] + ], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() ); - $this->assertEquals(array(), $message->getHeaders()); + $this->assertEquals([], $message->getHeaders()); - $this->assertEquals(array(), $message->getHeader('Content-Type')); + $this->assertEquals([], $message->getHeader('Content-Type')); $this->assertEquals('', $message->getHeaderLine('Content-Type')); $this->assertFalse($message->hasHeader('Content-Type')); - $new = $message->withHeader('Empty', array()); + $new = $message->withHeader('Empty', []); $this->assertSame($message, $new); $this->assertFalse($new->hasHeader('Empty')); - $new = $message->withAddedHeader('Empty', array()); + $new = $message->withAddedHeader('Empty', []); $this->assertSame($message, $new); $this->assertFalse($new->hasHeader('Empty')); @@ -178,16 +178,16 @@ public function testHeaderWithMultipleValuesAcrossMixedCaseNamesInConstructorMer { $message = new MessageMock( '1.1', - array( + [ 'SET-Cookie' => 'a=1', - 'set-cookie' => array('b=2'), - 'set-COOKIE' => array() - ), + 'set-cookie' => ['b=2'], + 'set-COOKIE' => [] + ], $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(['set-cookie' => ['a=1', 'b=2']], $message->getHeaders()); + $this->assertEquals(['a=1', 'b=2'], $message->getHeader('Set-Cookie')); } public function testWithBodyReturnsNewInstanceWhenBodyIsChanged() @@ -195,7 +195,7 @@ public function testWithBodyReturnsNewInstanceWhenBodyIsChanged() $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); $message = new MessageMock( '1.1', - array(), + [], $body ); @@ -211,7 +211,7 @@ public function testWithBodyReturnsSameInstanceWhenBodyIsUnchanged() $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); $message = new MessageMock( '1.1', - array(), + [], $body ); diff --git a/tests/Io/AbstractRequestTest.php b/tests/Io/AbstractRequestTest.php index 7ff4a9a5..24990622 100644 --- a/tests/Io/AbstractRequestTest.php +++ b/tests/Io/AbstractRequestTest.php @@ -36,7 +36,7 @@ public function testCtorWithInvalidUriThrows() new RequestMock( 'GET', null, - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -47,12 +47,12 @@ 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()); + $this->assertEquals(['Host' => ['example.com']], $request->getHeaders()); } public function testGetHeadersReturnsHostHeaderFromUriWithCustomHttpPort() @@ -60,12 +60,12 @@ 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()); + $this->assertEquals(['Host' => ['example.com:8080']], $request->getHeaders()); } public function testGetHeadersReturnsHostHeaderFromUriWithCustomPortHttpOnHttpsPort() @@ -73,12 +73,12 @@ public function testGetHeadersReturnsHostHeaderFromUriWithCustomPortHttpOnHttpsP $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()); + $this->assertEquals(['Host' => ['example.com:443']], $request->getHeaders()); } public function testGetHeadersReturnsHostHeaderFromUriWithCustomPortHttpsOnHttpPort() @@ -86,12 +86,12 @@ public function testGetHeadersReturnsHostHeaderFromUriWithCustomPortHttpsOnHttpP $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()); + $this->assertEquals(['Host' => ['example.com:80']], $request->getHeaders()); } public function testGetHeadersReturnsHostHeaderFromUriWithoutDefaultHttpPort() @@ -99,12 +99,12 @@ 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()); + $this->assertEquals(['Host' => ['example.com']], $request->getHeaders()); } public function testGetHeadersReturnsHostHeaderFromUriWithoutDefaultHttpsPort() @@ -112,12 +112,12 @@ 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()); + $this->assertEquals(['Host' => ['example.com']], $request->getHeaders()); } public function testGetHeadersReturnsHostHeaderFromUriBeforeOtherHeadersExplicitlyGiven() @@ -125,14 +125,14 @@ public function testGetHeadersReturnsHostHeaderFromUriBeforeOtherHeadersExplicit $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()); + $this->assertEquals(['Host' => ['example.com'], 'User-Agent' => ['demo']], $request->getHeaders()); } public function testGetHeadersReturnsHostHeaderFromHeadersExplicitlyGiven() @@ -140,14 +140,14 @@ 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()); + $this->assertEquals(['Host' => ['example.com:8080']], $request->getHeaders()); } public function testGetHeadersReturnsHostHeaderFromUriWhenHeadersExplicitlyGivenContainEmptyHostArray() @@ -155,14 +155,14 @@ public function testGetHeadersReturnsHostHeaderFromUriWhenHeadersExplicitlyGiven $request = new RequestMock( 'GET', 'https://example.com/', - array( - 'Host' => array() - ), + [ + 'Host' => [] + ], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); - $this->assertEquals(array('Host' => array('example.com')), $request->getHeaders()); + $this->assertEquals(['Host' => ['example.com']], $request->getHeaders()); } public function testGetRequestTargetReturnsPathAndQueryFromUri() @@ -170,7 +170,7 @@ public function testGetRequestTargetReturnsPathAndQueryFromUri() $request = new RequestMock( 'GET', 'http://example.com/demo?name=Alice', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -183,7 +183,7 @@ public function testGetRequestTargetReturnsSlashOnlyIfUriHasNoPathOrQuery() $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -196,7 +196,7 @@ public function testGetRequestTargetReturnsRequestTargetInAbsoluteFormIfGivenExp $request = new RequestMock( 'GET', 'http://example.com/demo?name=Alice', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -210,7 +210,7 @@ public function testWithRequestTargetReturnsNewInstanceWhenRequestTargetIsChange $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -226,7 +226,7 @@ public function testWithRequestTargetReturnsSameInstanceWhenRequestTargetIsUncha $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -242,7 +242,7 @@ public function testWithMethodReturnsNewInstanceWhenMethodIsChanged() $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -258,7 +258,7 @@ public function testWithMethodReturnsSameInstanceWhenMethodIsUnchanged() $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -275,7 +275,7 @@ public function testGetUriReturnsUriInstanceGivenToCtor() $request = new RequestMock( 'GET', $uri, - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -288,7 +288,7 @@ public function testGetUriReturnsUriInstanceForUriStringGivenToCtor() $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -303,7 +303,7 @@ public function testWithUriReturnsNewInstanceWhenUriIsChanged() $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -323,7 +323,7 @@ public function testWithUriReturnsSameInstanceWhenUriIsUnchanged() $request = new RequestMock( 'GET', $uri, - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -338,7 +338,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderChangedIfUriContainsH $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -348,7 +348,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderChangedIfUriContainsH $this->assertNotSame($request, $new); $this->assertEquals('http://localhost/', (string) $new->getUri()); - $this->assertEquals(array('Host' => array('localhost')), $new->getHeaders()); + $this->assertEquals(['Host' => ['localhost']], $new->getHeaders()); } public function testWithUriReturnsNewInstanceWithHostHeaderChangedIfUriContainsHostWithCustomPort() @@ -356,7 +356,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderChangedIfUriContainsH $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -366,7 +366,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderChangedIfUriContainsH $this->assertNotSame($request, $new); $this->assertEquals('http://localhost:8080/', (string) $new->getUri()); - $this->assertEquals(array('Host' => array('localhost:8080')), $new->getHeaders()); + $this->assertEquals(['Host' => ['localhost:8080']], $new->getHeaders()); } public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderBeforeOthersIfUriContainsHost() @@ -374,9 +374,9 @@ public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderBef $request = new RequestMock( 'GET', 'http://example.com/', - array( + [ 'User-Agent' => 'test' - ), + ], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -387,7 +387,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderBef $this->assertNotSame($request, $new); $this->assertEquals('http://localhost/', (string) $new->getUri()); - $this->assertEquals(array('Host' => array('localhost'), 'User-Agent' => array('test')), $new->getHeaders()); + $this->assertEquals(['Host' => ['localhost'], 'User-Agent' => ['test']], $new->getHeaders()); } public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfUriContainsNoHost() @@ -395,7 +395,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfUriContain $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -405,7 +405,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfUriContain $this->assertNotSame($request, $new); $this->assertEquals('/path', (string) $new->getUri()); - $this->assertEquals(array('Host' => array('example.com')), $new->getHeaders()); + $this->assertEquals(['Host' => ['example.com']], $new->getHeaders()); } public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfPreserveHostIsTrue() @@ -413,7 +413,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfPreserveHo $request = new RequestMock( 'GET', 'http://example.com/', - array(), + [], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -423,7 +423,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfPreserveHo $this->assertNotSame($request, $new); $this->assertEquals('http://localhost/', (string) $new->getUri()); - $this->assertEquals(array('Host' => array('example.com')), $new->getHeaders()); + $this->assertEquals(['Host' => ['example.com']], $new->getHeaders()); } public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderNoMatterIfPreserveHostIsTrue() @@ -431,9 +431,9 @@ public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderNoM $request = new RequestMock( 'GET', 'http://example.com/', - array( + [ 'User-Agent' => 'test' - ), + ], $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), '1.1' ); @@ -444,6 +444,6 @@ public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderNoM $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()); + $this->assertEquals(['Host' => ['example.com'], 'User-Agent' => ['test']], $new->getHeaders()); } } diff --git a/tests/Io/BufferedBodyTest.php b/tests/Io/BufferedBodyTest.php index 01154e71..4f5d042a 100644 --- a/tests/Io/BufferedBodyTest.php +++ b/tests/Io/BufferedBodyTest.php @@ -288,7 +288,7 @@ public function testGetMetadataWithoutKeyReturnsEmptyArray() { $stream = new BufferedBody('hello'); - $this->assertEquals(array(), $stream->getMetadata()); + $this->assertEquals([], $stream->getMetadata()); } public function testGetMetadataWithKeyReturnsNull() diff --git a/tests/Io/ChunkedDecoderTest.php b/tests/Io/ChunkedDecoderTest.php index 822ceaa6..5168f2d0 100644 --- a/tests/Io/ChunkedDecoderTest.php +++ b/tests/Io/ChunkedDecoderTest.php @@ -26,12 +26,12 @@ public function testSimpleChunk() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableNever()); - $this->input->emit('data', array("5\r\nhello\r\n")); + $this->input->emit('data', ["5\r\nhello\r\n"]); } public function testTwoChunks() { - $buffer = array(); + $buffer = []; $this->parser->on('data', function ($data) use (&$buffer) { $buffer[] = $data; }); @@ -39,9 +39,9 @@ public function testTwoChunks() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableNever()); - $this->input->emit('data', array("5\r\nhello\r\n3\r\nbla\r\n")); + $this->input->emit('data', ["5\r\nhello\r\n3\r\nbla\r\n"]); - $this->assertEquals(array('hello', 'bla'), $buffer); + $this->assertEquals(['hello', 'bla'], $buffer); } public function testEnd() @@ -50,12 +50,12 @@ public function testEnd() $this->parser->on('close', $this->expectCallableOnce()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("0\r\n\r\n")); + $this->input->emit('data', ["0\r\n\r\n"]); } public function testParameterWithEnd() { - $buffer = array(); + $buffer = []; $this->parser->on('data', function ($data) use (&$buffer) { $buffer[] = $data; }); @@ -64,9 +64,9 @@ public function testParameterWithEnd() $this->parser->on('close', $this->expectCallableOnce()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("5\r\nhello\r\n3\r\nbla\r\n0\r\n\r\n")); + $this->input->emit('data', ["5\r\nhello\r\n3\r\nbla\r\n0\r\n\r\n"]); - $this->assertEquals(array('hello', 'bla'), $buffer); + $this->assertEquals(['hello', 'bla'], $buffer); } public function testInvalidChunk() @@ -76,7 +76,7 @@ public function testInvalidChunk() $this->parser->on('close', $this->expectCallableOnce()); $this->parser->on('error', $this->expectCallableOnce()); - $this->input->emit('data', array("bla\r\n")); + $this->input->emit('data', ["bla\r\n"]); } public function testNeverEnd() @@ -85,7 +85,7 @@ public function testNeverEnd() $this->parser->on('close', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("0\r\n")); + $this->input->emit('data', ["0\r\n"]); } public function testWrongChunkHex() @@ -94,7 +94,7 @@ public function testWrongChunkHex() $this->parser->on('close', $this->expectCallableOnce()); $this->parser->on('end', $this->expectCallableNever()); - $this->input->emit('data', array("2\r\na\r\n5\r\nhello\r\n")); + $this->input->emit('data', ["2\r\na\r\n5\r\nhello\r\n"]); } public function testSplittedChunk() @@ -104,8 +104,8 @@ public function testSplittedChunk() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("4\r\n")); - $this->input->emit('data', array("welt\r\n")); + $this->input->emit('data', ["4\r\n"]); + $this->input->emit('data', ["welt\r\n"]); } public function testSplittedHeader() @@ -115,8 +115,8 @@ public function testSplittedHeader() $this->parser->on('end', $this->expectCallableNever());# $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("4")); - $this->input->emit('data', array("\r\nwelt\r\n")); + $this->input->emit('data', ["4"]); + $this->input->emit('data', ["\r\nwelt\r\n"]); } public function testSplittedBoth() @@ -126,14 +126,14 @@ public function testSplittedBoth() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("4")); - $this->input->emit('data', array("\r\n")); - $this->input->emit('data', array("welt\r\n")); + $this->input->emit('data', ["4"]); + $this->input->emit('data', ["\r\n"]); + $this->input->emit('data', ["welt\r\n"]); } public function testCompletlySplitted() { - $buffer = array(); + $buffer = []; $this->parser->on('data', function ($data) use (&$buffer) { $buffer[] = $data; }); @@ -142,17 +142,17 @@ public function testCompletlySplitted() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("4")); - $this->input->emit('data', array("\r\n")); - $this->input->emit('data', array("we")); - $this->input->emit('data', array("lt\r\n")); + $this->input->emit('data', ["4"]); + $this->input->emit('data', ["\r\n"]); + $this->input->emit('data', ["we"]); + $this->input->emit('data', ["lt\r\n"]); - $this->assertEquals(array('we', 'lt'), $buffer); + $this->assertEquals(['we', 'lt'], $buffer); } public function testMixed() { - $buffer = array(); + $buffer = []; $this->parser->on('data', function ($data) use (&$buffer) { $buffer[] = $data; }); @@ -161,17 +161,17 @@ public function testMixed() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("4")); - $this->input->emit('data', array("\r\n")); - $this->input->emit('data', array("welt\r\n")); - $this->input->emit('data', array("5\r\nhello\r\n")); + $this->input->emit('data', ["4"]); + $this->input->emit('data', ["\r\n"]); + $this->input->emit('data', ["welt\r\n"]); + $this->input->emit('data', ["5\r\nhello\r\n"]); - $this->assertEquals(array('welt', 'hello'), $buffer); + $this->assertEquals(['welt', 'hello'], $buffer); } public function testBigger() { - $buffer = array(); + $buffer = []; $this->parser->on('data', function ($data) use (&$buffer) { $buffer[] = $data; }); @@ -180,18 +180,18 @@ public function testBigger() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("1")); - $this->input->emit('data', array("0")); - $this->input->emit('data', array("\r\n")); - $this->input->emit('data', array("abcdeabcdeabcdea\r\n")); - $this->input->emit('data', array("5\r\nhello\r\n")); + $this->input->emit('data', ["1"]); + $this->input->emit('data', ["0"]); + $this->input->emit('data', ["\r\n"]); + $this->input->emit('data', ["abcdeabcdeabcdea\r\n"]); + $this->input->emit('data', ["5\r\nhello\r\n"]); - $this->assertEquals(array('abcdeabcdeabcdea', 'hello'), $buffer); + $this->assertEquals(['abcdeabcdeabcdea', 'hello'], $buffer); } public function testOneUnfinished() { - $buffer = array(); + $buffer = []; $this->parser->on('data', function ($data) use (&$buffer) { $buffer[] = $data; }); @@ -200,11 +200,11 @@ public function testOneUnfinished() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("3\r\n")); - $this->input->emit('data', array("bla\r\n")); - $this->input->emit('data', array("5\r\nhello")); + $this->input->emit('data', ["3\r\n"]); + $this->input->emit('data', ["bla\r\n"]); + $this->input->emit('data', ["5\r\nhello"]); - $this->assertEquals(array('bla', 'hello'), $buffer); + $this->assertEquals(['bla', 'hello'], $buffer); } public function testChunkIsBiggerThenExpected() @@ -214,8 +214,8 @@ public function testChunkIsBiggerThenExpected() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableOnce()); - $this->input->emit('data', array("5\r\n")); - $this->input->emit('data', array("hello world\r\n")); + $this->input->emit('data', ["5\r\n"]); + $this->input->emit('data', ["hello world\r\n"]); } public function testHandleUnexpectedEnd() @@ -235,7 +235,7 @@ public function testExtensionWillBeIgnored() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("3;hello=world;foo=bar\r\nbla")); + $this->input->emit('data', ["3;hello=world;foo=bar\r\nbla"]); } public function testChunkHeaderIsTooBig() @@ -249,7 +249,7 @@ public function testChunkHeaderIsTooBig() for ($i = 0; $i < 1025; $i++) { $data .= 'a'; } - $this->input->emit('data', array($data)); + $this->input->emit('data', [$data]); } public function testChunkIsMaximumSize() @@ -265,7 +265,7 @@ public function testChunkIsMaximumSize() } $data .= "\r\n"; - $this->input->emit('data', array($data)); + $this->input->emit('data', [$data]); } public function testLateCrlf() @@ -275,9 +275,9 @@ public function testLateCrlf() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("4\r\nlate")); - $this->input->emit('data', array("\r")); - $this->input->emit('data', array("\n")); + $this->input->emit('data', ["4\r\nlate"]); + $this->input->emit('data', ["\r"]); + $this->input->emit('data', ["\n"]); } public function testNoCrlfInChunk() @@ -287,7 +287,7 @@ public function testNoCrlfInChunk() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableOnce()); - $this->input->emit('data', array("2\r\nno crlf")); + $this->input->emit('data', ["2\r\nno crlf"]); } public function testNoCrlfInChunkSplitted() @@ -297,10 +297,10 @@ public function testNoCrlfInChunkSplitted() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableOnce()); - $this->input->emit('data', array("2\r\n")); - $this->input->emit('data', array("no")); - $this->input->emit('data', array("further")); - $this->input->emit('data', array("clrf")); + $this->input->emit('data', ["2\r\n"]); + $this->input->emit('data', ["no"]); + $this->input->emit('data', ["further"]); + $this->input->emit('data', ["clrf"]); } public function testEmitEmptyChunkBody() @@ -310,9 +310,9 @@ public function testEmitEmptyChunkBody() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("2\r\n")); - $this->input->emit('data', array("")); - $this->input->emit('data', array("")); + $this->input->emit('data', ["2\r\n"]); + $this->input->emit('data', [""]); + $this->input->emit('data', [""]); } public function testEmitCrlfAsChunkBody() @@ -322,9 +322,9 @@ public function testEmitCrlfAsChunkBody() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableNever()); - $this->input->emit('data', array("2\r\n")); - $this->input->emit('data', array("\r\n")); - $this->input->emit('data', array("\r\n")); + $this->input->emit('data', ["2\r\n"]); + $this->input->emit('data', ["\r\n"]); + $this->input->emit('data', ["\r\n"]); } public function testNegativeHeader() @@ -334,7 +334,7 @@ public function testNegativeHeader() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableOnce()); - $this->input->emit('data', array("-2\r\n")); + $this->input->emit('data', ["-2\r\n"]); } public function testHexDecimalInBodyIsPotentialThread() @@ -344,7 +344,7 @@ public function testHexDecimalInBodyIsPotentialThread() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableOnce()); - $this->input->emit('data', array("4\r\ntest5\r\nworld")); + $this->input->emit('data', ["4\r\ntest5\r\nworld"]); } public function testHexDecimalInBodyIsPotentialThreadSplitted() @@ -354,17 +354,17 @@ public function testHexDecimalInBodyIsPotentialThreadSplitted() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('error', $this->expectCallableOnce()); - $this->input->emit('data', array("4")); - $this->input->emit('data', array("\r\n")); - $this->input->emit('data', array("test")); - $this->input->emit('data', array("5")); - $this->input->emit('data', array("\r\n")); - $this->input->emit('data', array("world")); + $this->input->emit('data', ["4"]); + $this->input->emit('data', ["\r\n"]); + $this->input->emit('data', ["test"]); + $this->input->emit('data', ["5"]); + $this->input->emit('data', ["\r\n"]); + $this->input->emit('data', ["world"]); } public function testEmitSingleCharacter() { - $buffer = array(); + $buffer = []; $this->parser->on('data', function ($data) use (&$buffer) { $buffer[] = $data; }); @@ -375,10 +375,10 @@ public function testEmitSingleCharacter() $array = str_split("4\r\ntest\r\n0\r\n\r\n"); foreach ($array as $character) { - $this->input->emit('data', array($character)); + $this->input->emit('data', [$character]); } - $this->assertEquals(array('t', 'e', 's', 't'), $buffer); + $this->assertEquals(['t', 'e', 's', 't'], $buffer); } public function testHandleError() @@ -387,7 +387,7 @@ public function testHandleError() $this->parser->on('close', $this->expectCallableOnce()); $this->parser->on('end', $this->expectCallableNever()); - $this->input->emit('error', array(new \RuntimeException())); + $this->input->emit('error', [new \RuntimeException()]); $this->assertFalse($this->parser->isReadable()); } @@ -425,7 +425,7 @@ public function testHandleClose() $this->parser->on('close', $this->expectCallableOnce()); $this->input->close(); - $this->input->emit('end', array()); + $this->input->emit('end', []); $this->assertFalse($this->parser->isReadable()); } @@ -445,7 +445,7 @@ public function testOutputStreamCanCloseInputStream() public function testLeadingZerosWillBeIgnored() { - $buffer = array(); + $buffer = []; $this->parser->on('data', function ($data) use (&$buffer) { $buffer[] = $data; }); @@ -454,10 +454,10 @@ public function testLeadingZerosWillBeIgnored() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableNever()); - $this->input->emit('data', array("00005\r\nhello\r\n")); - $this->input->emit('data', array("0000b\r\nhello world\r\n")); + $this->input->emit('data', ["00005\r\nhello\r\n"]); + $this->input->emit('data', ["0000b\r\nhello world\r\n"]); - $this->assertEquals(array('hello', 'hello world'), $buffer); + $this->assertEquals(['hello', 'hello world'], $buffer); } public function testLeadingZerosInEndChunkWillBeIgnored() @@ -467,7 +467,7 @@ public function testLeadingZerosInEndChunkWillBeIgnored() $this->parser->on('end', $this->expectCallableOnce()); $this->parser->on('close', $this->expectCallableOnce()); - $this->input->emit('data', array("0000\r\n\r\n")); + $this->input->emit('data', ["0000\r\n\r\n"]); } public function testAdditionalWhitespaceInEndChunkWillBeIgnored() @@ -477,7 +477,7 @@ public function testAdditionalWhitespaceInEndChunkWillBeIgnored() $this->parser->on('end', $this->expectCallableOnce()); $this->parser->on('close', $this->expectCallableOnce()); - $this->input->emit('data', array(" 0 \r\n\r\n")); + $this->input->emit('data', [" 0 \r\n\r\n"]); } public function testEndChunkWithTrailersWillBeIgnored() @@ -487,7 +487,7 @@ public function testEndChunkWithTrailersWillBeIgnored() $this->parser->on('end', $this->expectCallableOnce()); $this->parser->on('close', $this->expectCallableOnce()); - $this->input->emit('data', array("0\r\nFoo: bar\r\n\r\n")); + $this->input->emit('data', ["0\r\nFoo: bar\r\n\r\n"]); } public function testEndChunkWithMultipleTrailersWillBeIgnored() @@ -497,7 +497,7 @@ public function testEndChunkWithMultipleTrailersWillBeIgnored() $this->parser->on('end', $this->expectCallableOnce()); $this->parser->on('close', $this->expectCallableOnce()); - $this->input->emit('data', array("0\r\nFoo: a\r\nBar: b\r\nBaz: c\r\n\r\n")); + $this->input->emit('data', ["0\r\nFoo: a\r\nBar: b\r\nBaz: c\r\n\r\n"]); } public function testLeadingZerosInInvalidChunk() @@ -507,7 +507,7 @@ public function testLeadingZerosInInvalidChunk() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableOnce()); - $this->input->emit('data', array("0000hello\r\n\r\n")); + $this->input->emit('data', ["0000hello\r\n\r\n"]); } public function testEmptyHeaderLeadsToError() @@ -517,7 +517,7 @@ public function testEmptyHeaderLeadsToError() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableOnce()); - $this->input->emit('data', array("\r\n\r\n")); + $this->input->emit('data', ["\r\n\r\n"]); } public function testEmptyHeaderAndFilledBodyLeadsToError() @@ -527,7 +527,7 @@ public function testEmptyHeaderAndFilledBodyLeadsToError() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableOnce()); - $this->input->emit('data', array("\r\nhello\r\n")); + $this->input->emit('data', ["\r\nhello\r\n"]); } public function testUpperCaseHexWillBeHandled() @@ -537,7 +537,7 @@ public function testUpperCaseHexWillBeHandled() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableNever()); - $this->input->emit('data', array("A\r\n0123456790\r\n")); + $this->input->emit('data', ["A\r\n0123456790\r\n"]); } public function testLowerCaseHexWillBeHandled() @@ -547,7 +547,7 @@ public function testLowerCaseHexWillBeHandled() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableNever()); - $this->input->emit('data', array("a\r\n0123456790\r\n")); + $this->input->emit('data', ["a\r\n0123456790\r\n"]); } public function testMixedUpperAndLowerCaseHexValuesInHeaderWillBeHandled() @@ -559,6 +559,6 @@ public function testMixedUpperAndLowerCaseHexValuesInHeaderWillBeHandled() $this->parser->on('end', $this->expectCallableNever()); $this->parser->on('close', $this->expectCallableNever()); - $this->input->emit('data', array("aA\r\n" . $data . "\r\n")); + $this->input->emit('data', ["aA\r\n" . $data . "\r\n"]); } } diff --git a/tests/Io/ChunkedEncoderTest.php b/tests/Io/ChunkedEncoderTest.php index 87ce44c4..96b97848 100644 --- a/tests/Io/ChunkedEncoderTest.php +++ b/tests/Io/ChunkedEncoderTest.php @@ -23,19 +23,19 @@ public function setUpChunkedStream() public function testChunked() { $this->chunkedStream->on('data', $this->expectCallableOnceWith("5\r\nhello\r\n")); - $this->input->emit('data', array('hello')); + $this->input->emit('data', ['hello']); } public function testEmptyString() { $this->chunkedStream->on('data', $this->expectCallableNever()); - $this->input->emit('data', array('')); + $this->input->emit('data', ['']); } public function testBiggerStringToCheckHexValue() { $this->chunkedStream->on('data', $this->expectCallableOnceWith("1a\r\nabcdefghijklmnopqrstuvwxyz\r\n")); - $this->input->emit('data', array('abcdefghijklmnopqrstuvwxyz')); + $this->input->emit('data', ['abcdefghijklmnopqrstuvwxyz']); } public function testHandleClose() @@ -52,7 +52,7 @@ public function testHandleError() $this->chunkedStream->on('error', $this->expectCallableOnce()); $this->chunkedStream->on('close', $this->expectCallableOnce()); - $this->input->emit('error', array(new \RuntimeException())); + $this->input->emit('error', [new \RuntimeException()]); $this->assertFalse($this->chunkedStream->isReadable()); } diff --git a/tests/Io/ClientConnectionManagerTest.php b/tests/Io/ClientConnectionManagerTest.php index 6aafa6db..88c5ff4f 100644 --- a/tests/Io/ClientConnectionManagerTest.php +++ b/tests/Io/ClientConnectionManagerTest.php @@ -7,6 +7,7 @@ use React\Promise\Promise; use React\Promise\PromiseInterface; use React\Tests\Http\TestCase; +use function React\Promise\resolve; class ClientConnectionManagerTest extends TestCase { @@ -100,51 +101,51 @@ public function testConnectReusesIdleConnectionFromPreviousKeepAliveCallWithoutU $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(); @@ -223,7 +224,7 @@ public function testConnectUsesConnectorForNewConnectionWhenPreviousConnectReuse $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)); + $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(resolve($secondConnection)); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -297,7 +298,7 @@ public function testConnectUsesConnectorForNewConnectionWhenIdleConnectionFromPr $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)); + $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(resolve($secondConnection)); $timerCallback = null; $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); @@ -334,34 +335,34 @@ public function testConnectUsesConnectorForNewConnectionWhenIdleConnectionFromPr $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)); + $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(resolve($secondConnection)); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); diff --git a/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index 9a5373a1..0df92961 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -12,6 +12,8 @@ use React\Stream\DuplexResourceStream; use React\Stream\ReadableStreamInterface; use React\Tests\Http\TestCase; +use function React\Promise\reject; +use function React\Promise\resolve; class ClientRequestStreamTest extends TestCase { @@ -22,25 +24,25 @@ public function testRequestShouldUseConnectionManagerWithUriFromRequestAndBindTo $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)); + $connectionManager->expects($this->once())->method('connect')->with($uri)->willReturn(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'))) + ['drain', $this->identicalTo([$request, 'handleDrain'])], + ['data', $this->identicalTo([$request, 'handleData'])], + ['end', $this->identicalTo([$request, 'handleEnd'])], + ['error', $this->identicalTo([$request, 'handleError'])], + ['close', $this->identicalTo([$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'))) + ['drain', $this->identicalTo([$request, 'handleDrain'])], + ['data', $this->identicalTo([$request, 'handleData'])], + ['end', $this->identicalTo([$request, 'handleEnd'])], + ['error', $this->identicalTo([$request, 'handleError'])], + ['close', $this->identicalTo([$request, 'close'])] ); $request->end(); @@ -54,7 +56,7 @@ public function testRequestShouldUseConnectionManagerWithUriFromRequestAndBindTo public function requestShouldEmitErrorIfConnectionFails() { $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); - $connectionManager->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException())); + $connectionManager->expects($this->once())->method('connect')->willReturn(reject(new \RuntimeException())); $requestData = new Request('GET', 'http://www.example.com/'); $request = new ClientRequestStream($connectionManager, $requestData); @@ -71,7 +73,7 @@ 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)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/'); $request = new ClientRequestStream($connectionManager, $requestData); @@ -89,7 +91,7 @@ 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)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/'); $request = new ClientRequestStream($connectionManager, $requestData); @@ -105,44 +107,44 @@ public static function provideInvalidRequest() { $request = new Request('GET' , "http://localhost/"); - return array( - array( + return [ + [ $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') - ) - ); + ] + ]; } /** @@ -168,7 +170,7 @@ 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)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/'); $request = new ClientRequestStream($connectionManager, $requestData); @@ -187,9 +189,9 @@ public function getRequestShouldSendAGetRequest() $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)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com/', array(), '', '1.0'); + $requestData = new Request('GET', 'http://www.example.com/', [], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); $request->end(); @@ -202,9 +204,9 @@ public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionCloseHea $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)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com/', ['Connection' => 'close'], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); $request->end(); @@ -217,9 +219,9 @@ public function getOptionsAsteriskShouldSendAOptionsRequestAsteriskRequestTarget $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)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('OPTIONS', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('OPTIONS', 'http://www.example.com/', ['Connection' => 'close'], '', '1.1'); $requestData = $requestData->withRequestTarget('*'); $request = new ClientRequestStream($connectionManager, $requestData); @@ -233,16 +235,15 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsCon $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)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com/', ['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('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableNever()); + $body->on('end', $this->expectCallableOnce()); + $body->on('close', $this->expectCallableOnce()); }); $request->on('close', $this->expectCallableOnce()); @@ -258,16 +259,15 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsSta $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)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com/', ['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('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableNever()); + $body->on('end', $this->expectCallableOnce()); + $body->on('close', $this->expectCallableOnce()); }); $request->on('close', $this->expectCallableOnce()); @@ -283,16 +283,15 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsSta $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)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com/', ['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('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableNever()); + $body->on('end', $this->expectCallableOnce()); + $body->on('close', $this->expectCallableOnce()); }); $request->on('close', $this->expectCallableOnce()); @@ -308,16 +307,15 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenRequestMethodIsHead $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)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('HEAD', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('HEAD', 'http://www.example.com/', ['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('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableNever()); + $body->on('end', $this->expectCallableOnce()); + $body->on('close', $this->expectCallableOnce()); }); $request->on('close', $this->expectCallableOnce()); @@ -333,16 +331,15 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons $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)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com/', ['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('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableOnceWith('OK')); + $body->on('end', $this->expectCallableOnce()); + $body->on('close', $this->expectCallableOnce()); }); $request->on('close', $this->expectCallableOnce()); @@ -358,16 +355,15 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResp $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('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com/', ['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('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableNever()); + $body->on('end', $this->expectCallableNever()); + $body->on('close', $this->expectCallableNever()); }); $request->on('close', $this->expectCallableNever()); @@ -383,16 +379,15 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndW $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('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com/', ['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('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableOnce('O')); + $body->on('end', $this->expectCallableNever()); + $body->on('close', $this->expectCallableNever()); }); $request->on('close', $this->expectCallableNever()); @@ -408,16 +403,15 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons $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)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com/', ['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('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableOnceWith('OK')); + $body->on('end', $this->expectCallableOnce()); + $body->on('close', $this->expectCallableOnce()); }); $request->on('close', $this->expectCallableOnce()); @@ -433,16 +427,15 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResp $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('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com/', ['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('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableNever()); + $body->on('end', $this->expectCallableNever()); + $body->on('close', $this->expectCallableNever()); }); $request->on('close', $this->expectCallableNever()); @@ -458,16 +451,15 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndW $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('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com/', ['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('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableOnceWith('O')); + $body->on('end', $this->expectCallableNever()); + $body->on('close', $this->expectCallableNever()); }); $request->on('close', $this->expectCallableNever()); @@ -483,16 +475,15 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndW $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('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com/', ['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('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableOnce('O')); + $body->on('end', $this->expectCallableNever()); + $body->on('close', $this->expectCallableNever()); }); $request->on('close', $this->expectCallableNever()); @@ -520,16 +511,15 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons })); $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('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'close'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com/', ['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('response', function (ResponseInterface $response, ReadableStreamInterface $body) { + $body->on('data', $this->expectCallableOnce('OK')); + $body->on('end', $this->expectCallableOnce()); + $body->on('close', $this->expectCallableOnce()); }); $request->on('close', $this->expectCallableOnce()); @@ -549,10 +539,10 @@ public function testStreamShouldReuseConnectionForHttp11ByDefault() $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('connect')->willReturn(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'); + $requestData = new Request('GET', 'http://www.example.com/', [], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -570,9 +560,9 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsConnection $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)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com/', array(), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com/', [], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -590,9 +580,9 @@ public function testStreamShouldNotReuseConnectionWhenRequestContainsConnectionC $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)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com/', array('Connection' => 'FOO, CLOSE, BAR'), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com/', ['Connection' => 'FOO, CLOSE, BAR'], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -610,9 +600,9 @@ public function testStreamShouldNotReuseConnectionForHttp10ByDefault() $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)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com/', array(), '', '1.0'); + $requestData = new Request('GET', 'http://www.example.com/', [], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -630,10 +620,10 @@ public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndRespon $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('connect')->willReturn(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'); + $requestData = new Request('GET', 'http://www.example.com/', ['Connection' => 'keep-alive'], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -651,10 +641,10 @@ public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndRespon $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('connect')->willReturn(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'); + $requestData = new Request('GET', 'http://www.example.com/', ['Connection' => 'FOO, KEEP-ALIVE, BAR'], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -684,9 +674,9 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsNoContentL })); $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('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com/', array(), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com/', [], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -719,9 +709,9 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsContentLen })); $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('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com/', array(), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com/', [], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -742,10 +732,10 @@ public function testStreamShouldReuseConnectionWhenResponseContainsTransferEncod $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('connect')->willReturn(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'); + $requestData = new Request('GET', 'http://www.example.com/', [], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -763,9 +753,9 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsTransferEn $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)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('GET', 'http://www.example.com/', array(), '', '1.1'); + $requestData = new Request('GET', 'http://www.example.com/', [], '', '1.1'); $request = new ClientRequestStream($connectionManager, $requestData); $request->on('close', $this->expectCallableOnce()); @@ -782,9 +772,9 @@ public function postRequestShouldSendAPostRequest() $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)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); - $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0'); + $requestData = new Request('POST', 'http://www.example.com/', [], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); $request->end('some post data'); @@ -799,15 +789,15 @@ 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")) + [$this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")], + [$this->identicalTo("post")], + [$this->identicalTo("data")] ); $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('connect')->willReturn(resolve($connection)); - $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0'); + $requestData = new Request('POST', 'http://www.example.com/', [], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); $request->write("some"); @@ -824,8 +814,8 @@ 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")) + [$this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsomepost$#")], + [$this->identicalTo("data")] )->willReturn( true ); @@ -834,7 +824,7 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent $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'); + $requestData = new Request('POST', 'http://www.example.com/', [], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); $this->assertFalse($request->write("some")); @@ -858,12 +848,12 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB { $connection = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() - ->setMethods(array('write')) + ->setMethods(['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")) + [$this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsomepost$#")], + [$this->identicalTo("data")] )->willReturn( false ); @@ -872,7 +862,7 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB $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'); + $requestData = new Request('POST', 'http://www.example.com/', [], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); $this->assertFalse($request->write("some")); @@ -897,15 +887,15 @@ 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")) + [$this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")], + [$this->identicalTo("post")], + [$this->identicalTo("data")] ); $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('connect')->willReturn(resolve($connection)); - $requestData = new Request('POST', 'http://www.example.com/', array(), '', '1.0'); + $requestData = new Request('POST', 'http://www.example.com/', [], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); $loop = $this @@ -916,9 +906,9 @@ public function pipeShouldPipeDataIntoTheRequestBody() $stream = new DuplexResourceStream($stream, $loop); $stream->pipe($request); - $stream->emit('data', array('some')); - $stream->emit('data', array('post')); - $stream->emit('data', array('data')); + $stream->emit('data', ['some']); + $stream->emit('data', ['post']); + $stream->emit('data', ['data']); $request->handleData("HTTP/1.0 200 OK\r\n"); $request->handleData("Content-Type: text/plain\r\n"); @@ -1044,7 +1034,7 @@ 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)); + $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/'); $request = new ClientRequestStream($connectionManager, $requestData); diff --git a/tests/Io/CloseProtectionStreamTest.php b/tests/Io/CloseProtectionStreamTest.php index 8490daff..4f3d35ca 100644 --- a/tests/Io/CloseProtectionStreamTest.php +++ b/tests/Io/CloseProtectionStreamTest.php @@ -27,7 +27,7 @@ public function testErrorWontCloseStream() $protection->on('error', $this->expectCallableOnce()); $protection->on('close', $this->expectCallableNever()); - $input->emit('error', array(new \RuntimeException())); + $input->emit('error', [new \RuntimeException()]); $this->assertTrue($protection->isReadable()); $this->assertTrue($input->isReadable()); @@ -91,7 +91,7 @@ public function testStopEmittingDataAfterClose() $protection->close(); - $input->emit('data', array('hello')); + $input->emit('data', ['hello']); $this->assertFalse($protection->isReadable()); $this->assertTrue($input->isReadable()); @@ -108,7 +108,7 @@ public function testErrorIsNeverCalledAfterClose() $protection->close(); - $input->emit('error', array(new \Exception())); + $input->emit('error', [new \Exception()]); $this->assertFalse($protection->isReadable()); $this->assertTrue($input->isReadable()); @@ -124,7 +124,7 @@ public function testEndWontBeEmittedAfterClose() $protection->close(); - $input->emit('end', array()); + $input->emit('end', []); $this->assertFalse($protection->isReadable()); $this->assertTrue($input->isReadable()); diff --git a/tests/Io/EmptyBodyStreamTest.php b/tests/Io/EmptyBodyStreamTest.php index 8430239d..3633ff81 100644 --- a/tests/Io/EmptyBodyStreamTest.php +++ b/tests/Io/EmptyBodyStreamTest.php @@ -65,7 +65,7 @@ public function testCloseTwiceEmitsCloseEventAndClearsListeners() $this->bodyStream->close(); $this->bodyStream->close(); - $this->assertEquals(array(), $this->bodyStream->listeners('close')); + $this->assertEquals([], $this->bodyStream->listeners('close')); } public function testTell() @@ -104,7 +104,7 @@ public function testGetContentsReturnsEmpy() public function testGetMetaDataWithoutKeyReturnsEmptyArray() { - $this->assertSame(array(), $this->bodyStream->getMetadata()); + $this->assertSame([], $this->bodyStream->getMetadata()); } public function testGetMetaDataWithKeyReturnsNull() diff --git a/tests/Io/HttpBodyStreamTest.php b/tests/Io/HttpBodyStreamTest.php index db21dcf8..1fd269b1 100644 --- a/tests/Io/HttpBodyStreamTest.php +++ b/tests/Io/HttpBodyStreamTest.php @@ -22,8 +22,8 @@ public function setUpBodyStream() public function testDataEmit() { - $this->bodyStream->on('data', $this->expectCallableOnce(array("hello"))); - $this->input->emit('data', array("hello")); + $this->bodyStream->on('data', $this->expectCallableOnce(["hello"])); + $this->input->emit('data', ["hello"]); } public function testPauseStream() @@ -58,7 +58,7 @@ public function testHandleClose() $this->bodyStream->on('close', $this->expectCallableOnce()); $this->input->close(); - $this->input->emit('end', array()); + $this->input->emit('end', []); $this->assertFalse($this->bodyStream->isReadable()); } @@ -67,11 +67,11 @@ public function testStopDataEmittingAfterClose() { $bodyStream = new HttpBodyStream($this->input, null); $bodyStream->on('close', $this->expectCallableOnce()); - $this->bodyStream->on('data', $this->expectCallableOnce(array("hello"))); + $this->bodyStream->on('data', $this->expectCallableOnce(["hello"])); - $this->input->emit('data', array("hello")); + $this->input->emit('data', ["hello"]); $bodyStream->close(); - $this->input->emit('data', array("world")); + $this->input->emit('data', ["world"]); } public function testHandleError() @@ -79,7 +79,7 @@ public function testHandleError() $this->bodyStream->on('error', $this->expectCallableOnce()); $this->bodyStream->on('close', $this->expectCallableOnce()); - $this->input->emit('error', array(new \RuntimeException())); + $this->input->emit('error', [new \RuntimeException()]); $this->assertFalse($this->bodyStream->isReadable()); } diff --git a/tests/Io/IniUtilTest.php b/tests/Io/IniUtilTest.php index 155c6ed2..0bc9a249 100644 --- a/tests/Io/IniUtilTest.php +++ b/tests/Io/IniUtilTest.php @@ -9,40 +9,40 @@ class IniUtilTest extends TestCase { public function provideIniSizes() { - return array( - array( + return [ + [ '1', 1, - ), - array( + ], + [ '10', 10, - ), - array( + ], + [ '1024', 1024, - ), - array( + ], + [ '1K', 1024, - ), - array( + ], + [ '1.5M', 1572864, - ), - array( + ], + [ '64M', 67108864, - ), - array( + ], + [ '8G', 8589934592, - ), - array( + ], + [ '1T', 1099511627776, - ), - ); + ], + ]; } /** @@ -60,14 +60,14 @@ public function testIniSizeToBytesWithInvalidSuffixReturnsNumberWithoutSuffix() public function provideInvalidInputIniSizeToBytes() { - return array( - array('-1G'), - array('0G'), - array('foo'), - array('fooK'), - array('1ooL'), - array('1ooL'), - ); + return [ + ['-1G'], + ['0G'], + ['foo'], + ['fooK'], + ['1ooL'], + ['1ooL'], + ]; } /** diff --git a/tests/Io/LengthLimitedStreamTest.php b/tests/Io/LengthLimitedStreamTest.php index b415269c..f1761a0b 100644 --- a/tests/Io/LengthLimitedStreamTest.php +++ b/tests/Io/LengthLimitedStreamTest.php @@ -24,7 +24,7 @@ public function testSimpleChunk() $stream = new LengthLimitedStream($this->input, 5); $stream->on('data', $this->expectCallableOnceWith('hello')); $stream->on('end', $this->expectCallableOnce()); - $this->input->emit('data', array("hello world")); + $this->input->emit('data', ["hello world"]); } public function testInputStreamKeepsEmitting() @@ -33,9 +33,9 @@ public function testInputStreamKeepsEmitting() $stream->on('data', $this->expectCallableOnceWith('hello')); $stream->on('end', $this->expectCallableOnce()); - $this->input->emit('data', array("hello world")); - $this->input->emit('data', array("world")); - $this->input->emit('data', array("world")); + $this->input->emit('data', ["hello world"]); + $this->input->emit('data', ["world"]); + $this->input->emit('data', ["world"]); } public function testZeroLengthInContentLengthWillIgnoreEmittedDataEvents() @@ -43,7 +43,7 @@ public function testZeroLengthInContentLengthWillIgnoreEmittedDataEvents() $stream = new LengthLimitedStream($this->input, 0); $stream->on('data', $this->expectCallableNever()); $stream->on('end', $this->expectCallableOnce()); - $this->input->emit('data', array("hello world")); + $this->input->emit('data', ["hello world"]); } public function testHandleError() @@ -52,7 +52,7 @@ public function testHandleError() $stream->on('error', $this->expectCallableOnce()); $stream->on('close', $this->expectCallableOnce()); - $this->input->emit('error', array(new \RuntimeException())); + $this->input->emit('error', [new \RuntimeException()]); $this->assertFalse($stream->isReadable()); } @@ -92,7 +92,7 @@ public function testHandleClose() $stream->on('close', $this->expectCallableOnce()); $this->input->close(); - $this->input->emit('end', array()); + $this->input->emit('end', []); $this->assertFalse($stream->isReadable()); } diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index e742ef6d..e46039bf 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -8,17 +8,19 @@ use React\Http\Io\MiddlewareRunner; use React\Http\Message\Response; use React\Http\Message\ServerRequest; -use React\Promise; +use React\Promise\Promise; use React\Promise\PromiseInterface; use React\Tests\Http\Middleware\ProcessStack; use React\Tests\Http\TestCase; +use function React\Async\await; +use function React\Promise\reject; final class MiddlewareRunnerTest extends TestCase { public function testEmptyMiddlewareStackThrowsException() { $request = new ServerRequest('GET', 'https://example.com/'); - $middlewares = array(); + $middlewares = []; $middlewareStack = new MiddlewareRunner($middlewares); $this->setExpectedException('RuntimeException', 'No middleware to run'); @@ -28,7 +30,7 @@ public function testEmptyMiddlewareStackThrowsException() public function testMiddlewareHandlerReceivesTwoArguments() { $args = null; - $middleware = new MiddlewareRunner(array( + $middleware = new MiddlewareRunner([ function (ServerRequestInterface $request, $next) use (&$args) { $args = func_num_args(); return $next($request); @@ -36,7 +38,7 @@ function (ServerRequestInterface $request, $next) use (&$args) { function (ServerRequestInterface $request) { return null; } - )); + ]); $request = new ServerRequest('GET', 'http://example.com/'); @@ -48,12 +50,12 @@ function (ServerRequestInterface $request) { public function testFinalHandlerReceivesOneArgument() { $args = null; - $middleware = new MiddlewareRunner(array( + $middleware = new MiddlewareRunner([ function (ServerRequestInterface $request) use (&$args) { $args = func_num_args(); return null; } - )); + ]); $request = new ServerRequest('GET', 'http://example.com/'); @@ -64,11 +66,11 @@ function (ServerRequestInterface $request) use (&$args) { public function testThrowsIfHandlerThrowsException() { - $middleware = new MiddlewareRunner(array( + $middleware = new MiddlewareRunner([ function (ServerRequestInterface $request) { throw new \RuntimeException('hello'); } - )); + ]); $request = new ServerRequest('GET', 'http://example.com/'); @@ -78,11 +80,11 @@ function (ServerRequestInterface $request) { public function testThrowsIfHandlerThrowsThrowable() { - $middleware = new MiddlewareRunner(array( + $middleware = new MiddlewareRunner([ function (ServerRequestInterface $request) { throw new \Error('hello'); } - )); + ]); $request = new ServerRequest('GET', 'http://example.com/'); @@ -99,42 +101,42 @@ public function provideProcessStackMiddlewares() $responseMiddleware = function () { return new Response(200); }; - return array( - array( - array( + return [ + [ + [ $processStackA, $responseMiddleware, - ), + ], 1, - ), - array( - array( + ], + [ + [ $processStackB, $processStackB, $responseMiddleware, - ), + ], 2, - ), - array( - array( + ], + [ + [ $processStackC, $processStackC, $processStackC, $responseMiddleware, - ), + ], 3, - ), - array( - array( + ], + [ + [ $processStackD, $processStackD, $processStackD, $processStackD, $responseMiddleware, - ), + ], 4, - ), - ); + ], + ]; } /** @@ -156,7 +158,7 @@ public function testProcessStack(array $middlewares, $expectedCallCount) $response = $middlewareStack($request); $this->assertTrue($response instanceof PromiseInterface); - $response = \React\Async\await($response); + $response = await($response); $this->assertTrue($response instanceof ResponseInterface); $this->assertSame(200, $response->getStatusCode()); @@ -172,18 +174,18 @@ public function testProcessStack(array $middlewares, $expectedCallCount) public function provideErrorHandler() { - return array( - array( + return [ + [ function (\Exception $e) { throw $e; } - ), - array( + ], + [ function (\Exception $e) { - return Promise\reject($e); + return reject($e); } - ) - ); + ] + ]; } /** @@ -195,7 +197,7 @@ public function testNextCanBeRunMoreThanOnceWithoutCorruptingTheMiddlewareStack( $retryCalled = 0; $error = null; $retry = function ($request, $next) use (&$error, &$retryCalled) { - $promise = new \React\Promise\Promise(function ($resolve) use ($request, $next) { + $promise = new Promise(function ($resolve) use ($request, $next) { $resolve($next($request)); }); @@ -209,7 +211,7 @@ public function testNextCanBeRunMoreThanOnceWithoutCorruptingTheMiddlewareStack( $response = new Response(); $called = 0; - $runner = new MiddlewareRunner(array( + $runner = new MiddlewareRunner([ $retry, function () use ($errorHandler, &$called, $response, $exception) { $called++; @@ -219,11 +221,11 @@ function () use ($errorHandler, &$called, $response, $exception) { return $response; } - )); + ]); $request = new ServerRequest('GET', 'https://example.com/'); - $this->assertSame($response, \React\Async\await($runner($request))); + $this->assertSame($response, await($runner($request))); $this->assertSame(1, $retryCalled); $this->assertSame(2, $called); $this->assertSame($exception, $error); @@ -231,15 +233,15 @@ function () use ($errorHandler, &$called, $response, $exception) { public function testMultipleRunsInvokeAllMiddlewareInCorrectOrder() { - $requests = array( + $requests = [ new ServerRequest('GET', 'https://example.com/1'), new ServerRequest('GET', 'https://example.com/2'), new ServerRequest('GET', 'https://example.com/3') - ); + ]; - $receivedRequests = array(); + $receivedRequests = []; - $middlewareRunner = new MiddlewareRunner(array( + $middlewareRunner = new MiddlewareRunner([ function (ServerRequestInterface $request, $next) use (&$receivedRequests) { $receivedRequests[] = 'middleware1: ' . $request->getUri(); return $next($request); @@ -250,16 +252,16 @@ function (ServerRequestInterface $request, $next) use (&$receivedRequests) { }, function (ServerRequestInterface $request) use (&$receivedRequests) { $receivedRequests[] = 'middleware3: ' . $request->getUri(); - return new \React\Promise\Promise(function () { }); + return new Promise(function () { }); } - )); + ]); foreach ($requests as $request) { $middlewareRunner($request); } $this->assertEquals( - array( + [ 'middleware1: https://example.com/1', 'middleware2: https://example.com/1', 'middleware3: https://example.com/1', @@ -269,20 +271,20 @@ function (ServerRequestInterface $request) use (&$receivedRequests) { 'middleware1: https://example.com/3', 'middleware2: https://example.com/3', 'middleware3: https://example.com/3' - ), + ], $receivedRequests ); } public function provideUncommonMiddlewareArrayFormats() { - return array( - array( + return [ + [ function () { $sequence = ''; // Numeric index gap - return array( + return [ 0 => function (ServerRequestInterface $request, $next) use (&$sequence) { $sequence .= 'A'; @@ -294,18 +296,18 @@ function () { return $next($request); }, 3 => function () use (&$sequence) { - return new Response(200, array(), $sequence . 'C'); + return new Response(200, [], $sequence . 'C'); }, - ); + ]; }, 'ABC', - ), - array( + ], + [ function () { $sequence = ''; // Reversed numeric indexes - return array( + return [ 2 => function (ServerRequestInterface $request, $next) use (&$sequence) { $sequence .= 'A'; @@ -317,18 +319,18 @@ function () { return $next($request); }, 0 => function () use (&$sequence) { - return new Response(200, array(), $sequence . 'C'); + return new Response(200, [], $sequence . 'C'); }, - ); + ]; }, 'ABC', - ), - array( + ], + [ function () { $sequence = ''; // Associative array - return array( + return [ 'middleware1' => function (ServerRequestInterface $request, $next) use (&$sequence) { $sequence .= 'A'; @@ -340,18 +342,18 @@ function () { return $next($request); }, 'middleware3' => function () use (&$sequence) { - return new Response(200, array(), $sequence . 'C'); + return new Response(200, [], $sequence . 'C'); }, - ); + ]; }, 'ABC', - ), - array( + ], + [ function () { $sequence = ''; // Associative array with empty or trimmable string keys - return array( + return [ '' => function (ServerRequestInterface $request, $next) use (&$sequence) { $sequence .= 'A'; @@ -363,18 +365,18 @@ function () { return $next($request); }, ' ' => function () use (&$sequence) { - return new Response(200, array(), $sequence . 'C'); + return new Response(200, [], $sequence . 'C'); }, - ); + ]; }, 'ABC', - ), - array( + ], + [ function () { $sequence = ''; // Mixed array keys - return array( + return [ '' => function (ServerRequestInterface $request, $next) use (&$sequence) { $sequence .= 'A'; @@ -391,13 +393,13 @@ function () { return $next($request); }, 2 => function () use (&$sequence) { - return new Response(200, array(), $sequence . 'D'); + return new Response(200, [], $sequence . 'D'); }, - ); + ]; }, 'ABCD', - ), - ); + ], + ]; } /** @@ -417,7 +419,7 @@ public function testUncommonMiddlewareArrayFormats($middlewareFactory, $expected public function testPendingNextRequestHandlersCanBeCalledConcurrently() { $called = 0; - $middleware = new MiddlewareRunner(array( + $middleware = new MiddlewareRunner([ function (RequestInterface $request, $next) { $first = $next($request); $second = $next($request); @@ -427,9 +429,9 @@ function (RequestInterface $request, $next) { function (RequestInterface $request) use (&$called) { ++$called; - return new Promise\Promise(function () { }); + return new Promise(function () { }); } - )); + ]); $request = new ServerRequest('GET', 'http://example.com/'); @@ -442,7 +444,7 @@ function (RequestInterface $request) use (&$called) { public function testCancelPendingNextHandler() { $once = $this->expectCallableOnce(); - $middleware = new MiddlewareRunner(array( + $middleware = new MiddlewareRunner([ function (RequestInterface $request, $next) { $ret = $next($request); $ret->cancel(); @@ -450,9 +452,9 @@ function (RequestInterface $request, $next) { return $ret; }, function (RequestInterface $request) use ($once) { - return new Promise\Promise(function () { }, $once); + return new Promise(function () { }, $once); } - )); + ]); $request = new ServerRequest('GET', 'http://example.com/'); @@ -462,14 +464,14 @@ function (RequestInterface $request) use ($once) { public function testCancelResultingPromiseWillCancelPendingNextHandler() { $once = $this->expectCallableOnce(); - $middleware = new MiddlewareRunner(array( + $middleware = new MiddlewareRunner([ function (RequestInterface $request, $next) { return $next($request); }, function (RequestInterface $request) use ($once) { - return new Promise\Promise(function () { }, $once); + return new Promise(function () { }, $once); } - )); + ]); $request = new ServerRequest('GET', 'http://example.com/'); diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index 7f1ec667..ba439760 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -22,9 +22,9 @@ public function testDoesNotParseWithoutMultipartFormDataContentType() $data .= "second\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data', - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -47,21 +47,21 @@ public function testPostKey() $data .= "second\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( - 'users' => array( + [ + 'users' => [ 'one' => 'single', 'two' => 'second', - ), - ), + ], + ], $parsedRequest->getParsedBody() ); } @@ -80,21 +80,21 @@ public function testPostWithQuotationMarkEncapsulatedBoundary() $data .= "second\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary="' . $boundary . '"', - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( - 'users' => array( + [ + 'users' => [ 'one' => 'single', 'two' => 'second', - ), - ), + ], + ], $parsedRequest->getParsedBody() ); } @@ -113,21 +113,21 @@ public function testPostFormDataNamesWithoutQuotationMark() $data .= "second\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary="' . $boundary . '"', - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( - 'users' => array( + [ + 'users' => [ 'one' => 'single', 'two' => 'second', - ), - ), + ], + ], $parsedRequest->getParsedBody() ); } @@ -146,18 +146,18 @@ public function testPostStringOverwritesMap() $data .= "2\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( + [ 'users' => '2' - ), + ], $parsedRequest->getParsedBody() ); } @@ -176,20 +176,20 @@ public function testPostMapOverwritesString() $data .= "2\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( - 'users' => array( + [ + 'users' => [ 'two' => '2', - ), - ), + ], + ], $parsedRequest->getParsedBody() ); } @@ -208,20 +208,20 @@ public function testPostVectorOverwritesString() $data .= "2\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( - 'users' => array( + [ + 'users' => [ '2', - ), - ), + ], + ], $parsedRequest->getParsedBody() ); } @@ -240,25 +240,25 @@ public function testPostDeeplyNestedArray() $data .= "2\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( - 'users' => array( - array( + [ + 'users' => [ + [ '1' - ), - array( + ], + [ '2' - ) - ), - ), + ] + ], + ], $parsedRequest->getParsedBody() ); } @@ -273,18 +273,18 @@ public function testEmptyPostValue() $data .= "\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( + [ 'key' => '' - ), + ], $parsedRequest->getParsedBody() ); } @@ -299,18 +299,18 @@ public function testEmptyPostKey() $data .= "value\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( + [ '' => 'value' - ), + ], $parsedRequest->getParsedBody() ); } @@ -325,22 +325,22 @@ public function testNestedPostKeyAssoc() $data .= "value\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( - 'a' => array( - 'b' => array( + [ + 'a' => [ + 'b' => [ 'c' => 'value' - ) - ) - ), + ] + ] + ], $parsedRequest->getParsedBody() ); } @@ -355,22 +355,22 @@ public function testNestedPostKeyVector() $data .= "value\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( - 'a' => array( - array( + [ + 'a' => [ + [ 'value' - ) - ) - ), + ] + ] + ], $parsedRequest->getParsedBody() ); } @@ -436,25 +436,25 @@ public function testFileUpload() $data .= "\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertSame( - array( + [ 'MAX_FILE_SIZE' => '12000', - 'users' => array( + 'users' => [ 'one' => 'single', 'two' => 'second', 0 => 'first in array', 1 => 'second in array', - ), + ], 'user' => 'single', 'user2' => 'second', - ), + ], $parsedRequest->getParsedBody() ); @@ -491,18 +491,18 @@ public function testInvalidDoubleContentDispositionUsesLast() $data .= "value\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); $this->assertEmpty($parsedRequest->getUploadedFiles()); $this->assertSame( - array( + [ 'key' => 'value' - ), + ], $parsedRequest->getParsedBody() ); } @@ -517,9 +517,9 @@ public function testInvalidMissingNewlineAfterValueWillBeIgnored() $data .= "value"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -537,9 +537,9 @@ public function testInvalidMissingValueWillBeIgnored() $data .= "\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -555,9 +555,9 @@ public function testInvalidMissingValueAndEndBoundaryWillBeIgnored() $data = "--$boundary\r\n"; $data .= "Content-Disposition: form-data; name=\"key\"\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -576,9 +576,9 @@ public function testInvalidContentDispositionMissingWillBeIgnored() $data .= "hello\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -597,9 +597,9 @@ public function testInvalidContentDispositionMissingValueWillBeIgnored() $data .= "value\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -618,9 +618,9 @@ public function testInvalidContentDispositionWithoutNameWillBeIgnored() $data .= "value\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -638,9 +638,9 @@ public function testInvalidMissingEndBoundaryWillBeIgnored() $data .= "\r\n"; $data .= "value\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -662,9 +662,9 @@ public function testInvalidUploadFileWithoutContentTypeUsesNullValue() $data .= "world\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -697,9 +697,9 @@ public function testInvalidUploadFileWithoutMultipleContentTypeUsesLastValue() $data .= "world\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -731,9 +731,9 @@ public function testUploadEmptyFile() $data .= "\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -765,9 +765,9 @@ public function testUploadTooLargeFile() $data .= "world\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(4); $parsedRequest = $parser->parse($request); @@ -798,9 +798,9 @@ public function testUploadTooLargeFileWithIniLikeSize() $data .= str_repeat('world', 1024) . "\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser('1K'); $parsedRequest = $parser->parse($request); @@ -831,9 +831,9 @@ public function testUploadNoFile() $data .= "\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -869,9 +869,9 @@ public function testUploadTooManyFilesReturnsTruncatedList() $data .= "world\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(100, 1); $parsedRequest = $parser->parse($request); @@ -910,9 +910,9 @@ public function testUploadTooManyFilesIgnoresEmptyFilesAndIncludesThemDespiteTru $data .= "world\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(100, 1); $parsedRequest = $parser->parse($request); @@ -953,9 +953,9 @@ public function testPostMaxFileSize() $data .= "\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -1008,9 +1008,9 @@ public function testPostMaxFileSizeIgnoredByFilesComingBeforeIt() $data .= "\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); $parsedRequest = $parser->parse($request); @@ -1040,9 +1040,9 @@ public function testWeOnlyParseTheAmountOfMultiPartChunksWeConfigured() $data .= str_repeat($chunk, $chunkCount); $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( + $request = new ServerRequest('POST', 'http://example.com/', [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + ], $data, 1.1); $parser = new MultipartParser(); diff --git a/tests/Io/PauseBufferStreamTest.php b/tests/Io/PauseBufferStreamTest.php index a9678a78..05bf3ee3 100644 --- a/tests/Io/PauseBufferStreamTest.php +++ b/tests/Io/PauseBufferStreamTest.php @@ -156,7 +156,7 @@ public function testErrorEventWillBePassedThroughAsIs() $stream->on('error', $this->expectCallableOnce()); $stream->on('close', $this->expectCallableOnce()); - $input->emit('error', array(new \RuntimeException())); + $input->emit('error', [new \RuntimeException()]); } public function testPausedStreamWillNotPassThroughErrorEvent() @@ -167,7 +167,7 @@ public function testPausedStreamWillNotPassThroughErrorEvent() $stream->pause(); $stream->on('error', $this->expectCallableNever()); $stream->on('close', $this->expectCallableNever()); - $input->emit('error', array(new \RuntimeException())); + $input->emit('error', [new \RuntimeException()]); } public function testPausedStreamWillPassThroughErrorEventOnResume() @@ -176,7 +176,7 @@ public function testPausedStreamWillPassThroughErrorEventOnResume() $stream = new PauseBufferStream($input); $stream->pause(); - $input->emit('error', array(new \RuntimeException())); + $input->emit('error', [new \RuntimeException()]); $stream->on('error', $this->expectCallableOnce()); $stream->on('close', $this->expectCallableOnce()); @@ -191,7 +191,7 @@ public function testPausedStreamWillNotPassThroughErrorEventOnExplicitClose() $stream->pause(); $stream->on('error', $this->expectCallableNever()); $stream->on('close', $this->expectCallableOnce()); - $input->emit('error', array(new \RuntimeException())); + $input->emit('error', [new \RuntimeException()]); $stream->close(); } diff --git a/tests/Io/ReadableBodyStreamTest.php b/tests/Io/ReadableBodyStreamTest.php index d89199ff..8ece6791 100644 --- a/tests/Io/ReadableBodyStreamTest.php +++ b/tests/Io/ReadableBodyStreamTest.php @@ -250,6 +250,6 @@ public function testPointlessGetMetadataReturnsNullWhenKeyIsGiven() public function testPointlessGetMetadataReturnsEmptyArrayWhenNoKeyIsGiven() { - $this->assertEquals(array(), $this->stream->getMetadata()); + $this->assertEquals([], $this->stream->getMetadata()); } } diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index 1ed994b7..d15d4e7f 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -19,14 +19,14 @@ public function testSplitShouldHappenOnDoubleCrlf() $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\n")); - $connection->emit('data', array("Host: example.com:80\r\n")); - $connection->emit('data', array("Connection: close\r\n")); + $connection->emit('data', ["GET / HTTP/1.1\r\n"]); + $connection->emit('data', ["Host: example.com:80\r\n"]); + $connection->emit('data', ["Connection: close\r\n"]); $parser->removeAllListeners(); $parser->on('headers', $this->expectCallableOnce()); - $connection->emit('data', array("\r\n")); + $connection->emit('data', ["\r\n"]); } public function testFeedInOneGo() @@ -40,7 +40,7 @@ public function testFeedInOneGo() $parser->handle($connection); $data = $this->createGetRequest(); - $connection->emit('data', array($data)); + $connection->emit('data', [$data]); } public function testFeedTwoRequestsOnSeparateConnections() @@ -60,8 +60,8 @@ public function testFeedTwoRequestsOnSeparateConnections() $parser->handle($connection2); $data = $this->createGetRequest(); - $connection1->emit('data', array($data)); - $connection2->emit('data', array($data)); + $connection1->emit('data', [$data]); + $connection2->emit('data', [$data]); $this->assertEquals(2, $called); } @@ -83,13 +83,13 @@ public function testHeadersEventShouldEmitRequestAndConnection() $parser->handle($connection); $data = $this->createGetRequest(); - $connection->emit('data', array($data)); + $connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $request); $this->assertSame('GET', $request->getMethod()); $this->assertEquals('http://example.com/', $request->getUri()); $this->assertSame('1.1', $request->getProtocolVersion()); - $this->assertSame(array('Host' => array('example.com'), 'Connection' => array('close')), $request->getHeaders()); + $this->assertSame(['Host' => ['example.com'], 'Connection' => ['close']], $request->getHeaders()); $this->assertSame($connection, $conn); } @@ -101,10 +101,9 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitEndForStreamingB $parser = new RequestHeaderParser($clock); $ended = false; - $that = $this; - $parser->on('headers', function (ServerRequestInterface $request) use (&$ended, $that) { + $parser->on('headers', function (ServerRequestInterface $request) use (&$ended) { $body = $request->getBody(); - $that->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); $body->on('end', function () use (&$ended) { $ended = true; @@ -115,7 +114,7 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitEndForStreamingB $parser->handle($connection); $data = "GET / HTTP/1.0\r\n\r\n"; - $connection->emit('data', array($data)); + $connection->emit('data', [$data]); $this->assertTrue($ended); } @@ -127,10 +126,9 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDat $parser = new RequestHeaderParser($clock); $buffer = ''; - $that = $this; - $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer, $that) { + $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer) { $body = $request->getBody(); - $that->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); $body->on('data', function ($chunk) use (&$buffer) { $buffer .= $chunk; @@ -145,7 +143,7 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDat $data = "POST / HTTP/1.0\r\nContent-Length: 11\r\n\r\n"; $data .= 'RANDOM DATA'; - $connection->emit('data', array($data)); + $connection->emit('data', [$data]); $this->assertSame('RANDOM DATA.', $buffer); } @@ -157,10 +155,9 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyWit $parser = new RequestHeaderParser($clock); $buffer = ''; - $that = $this; - $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer, $that) { + $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer) { $body = $request->getBody(); - $that->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); $body->on('data', function ($chunk) use (&$buffer) { $buffer .= $chunk; @@ -173,7 +170,7 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyWit $size = 10000; $data = "POST / HTTP/1.0\r\nContent-Length: $size\r\n\r\n"; $data .= str_repeat('x', $size); - $connection->emit('data', array($data)); + $connection->emit('data', [$data]); $this->assertSame($size, strlen($buffer)); } @@ -185,10 +182,9 @@ public function testHeadersEventShouldEmitRequestWhichShouldNotEmitStreamingBody $parser = new RequestHeaderParser($clock); $buffer = ''; - $that = $this; - $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer, $that) { + $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer) { $body = $request->getBody(); - $that->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); $body->on('data', function ($chunk) use (&$buffer) { $buffer .= $chunk; @@ -200,7 +196,7 @@ public function testHeadersEventShouldEmitRequestWhichShouldNotEmitStreamingBody $data = "POST / HTTP/1.0\r\n\r\n"; $data .= 'RANDOM DATA'; - $connection->emit('data', array($data)); + $connection->emit('data', [$data]); $this->assertSame('', $buffer); } @@ -212,10 +208,9 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDat $parser = new RequestHeaderParser($clock); $buffer = ''; - $that = $this; - $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer, $that) { + $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer) { $body = $request->getBody(); - $that->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); $body->on('data', function ($chunk) use (&$buffer) { $buffer .= $chunk; @@ -227,7 +222,7 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDat $data = "POST / HTTP/1.0\r\nContent-Length: 6\r\n\r\n"; $data .= 'RANDOM DATA'; - $connection->emit('data', array($data)); + $connection->emit('data', [$data]); $this->assertSame('RANDOM', $buffer); } @@ -247,17 +242,17 @@ public function testHeadersEventShouldParsePathAndQueryString() $parser->handle($connection); $data = $this->createAdvancedPostRequest(); - $connection->emit('data', array($data)); + $connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $request); $this->assertSame('POST', $request->getMethod()); $this->assertEquals('http://example.com/foo?bar=baz', $request->getUri()); $this->assertSame('1.1', $request->getProtocolVersion()); - $headers = array( - 'Host' => array('example.com'), - 'User-Agent' => array('react/alpha'), - 'Connection' => array('close'), - ); + $headers = [ + 'Host' => ['example.com'], + 'User-Agent' => ['react/alpha'], + 'Connection' => ['close'] + ]; $this->assertSame($headers, $request->getHeaders()); } @@ -272,11 +267,11 @@ public function testHeaderEventWithShouldApplyDefaultAddressFromLocalConnectionA $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress'))->getMock(); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('tcp://127.1.1.1:8000'); $parser->handle($connection); - $connection->emit('data', array("GET /foo HTTP/1.0\r\n\r\n")); + $connection->emit('data', ["GET /foo HTTP/1.0\r\n\r\n"]); $this->assertEquals('http://127.1.1.1:8000/foo', $request->getUri()); $this->assertFalse($request->hasHeader('Host')); @@ -293,11 +288,11 @@ public function testHeaderEventViaHttpsShouldApplyHttpsSchemeFromLocalTlsConnect $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress'))->getMock(); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('tls://127.1.1.1:8000'); $parser->handle($connection); - $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); $this->assertEquals('https://example.com/foo', $request->getUri()); $this->assertEquals('example.com', $request->getHeaderLine('Host')); @@ -321,7 +316,7 @@ public function testHeaderOverflowShouldEmitError() $parser->handle($connection); $data = str_repeat('A', 8193); - $connection->emit('data', array($data)); + $connection->emit('data', [$data]); $this->assertInstanceOf('OverflowException', $error); $this->assertSame('Maximum header size of 8192 exceeded.', $error->getMessage()); @@ -343,7 +338,7 @@ public function testInvalidEmptyRequestHeadersParseException() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("\r\n\r\n")); + $connection->emit('data', ["\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Unable to parse invalid request-line', $error->getMessage()); @@ -364,7 +359,7 @@ public function testInvalidMalformedRequestLineParseException() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET /\r\n\r\n")); + $connection->emit('data', ["GET /\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Unable to parse invalid request-line', $error->getMessage()); @@ -385,7 +380,7 @@ public function testInvalidMalformedRequestHeadersThrowsParseException() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\nHost : yes\r\n\r\n")); + $connection->emit('data', ["GET / HTTP/1.1\r\nHost : yes\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Unable to parse invalid request header fields', $error->getMessage()); @@ -406,7 +401,7 @@ public function testInvalidMalformedRequestHeadersWhitespaceThrowsParseException $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\nHost: yes\rFoo: bar\r\n\r\n")); + $connection->emit('data', ["GET / HTTP/1.1\r\nHost: yes\rFoo: bar\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Unable to parse invalid request header fields', $error->getMessage()); @@ -427,7 +422,7 @@ public function testInvalidAbsoluteFormSchemeEmitsError() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET tcp://example.com:80/ HTTP/1.0\r\n\r\n")); + $connection->emit('data', ["GET tcp://example.com:80/ HTTP/1.0\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); @@ -448,15 +443,15 @@ public function testOriginFormWithSchemeSeparatorInParam() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET /somepath?param=http://example.com HTTP/1.1\r\nHost: localhost\r\n\r\n")); + $connection->emit('data', ["GET /somepath?param=http://example.com HTTP/1.1\r\nHost: localhost\r\n\r\n"]); $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $request); $this->assertSame('GET', $request->getMethod()); $this->assertEquals('http://localhost/somepath?param=http://example.com', $request->getUri()); $this->assertSame('1.1', $request->getProtocolVersion()); - $headers = array( - 'Host' => array('localhost') - ); + $headers = [ + 'Host' => ['localhost'] + ]; $this->assertSame($headers, $request->getHeaders()); } @@ -475,7 +470,7 @@ public function testUriStartingWithColonSlashSlashFails() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET ://example.com:80/ HTTP/1.0\r\n\r\n")); + $connection->emit('data', ["GET ://example.com:80/ HTTP/1.0\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); @@ -496,7 +491,7 @@ public function testInvalidAbsoluteFormWithFragmentEmitsError() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET http://example.com:80/#home HTTP/1.0\r\n\r\n")); + $connection->emit('data', ["GET http://example.com:80/#home HTTP/1.0\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); @@ -517,7 +512,7 @@ public function testInvalidHeaderContainsFullUri() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\nHost: http://user:pass@host/\r\n\r\n")); + $connection->emit('data', ["GET / HTTP/1.1\r\nHost: http://user:pass@host/\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Invalid Host header value', $error->getMessage()); @@ -538,7 +533,7 @@ public function testInvalidAbsoluteFormWithHostHeaderEmpty() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET http://example.com/ HTTP/1.1\r\nHost: \r\n\r\n")); + $connection->emit('data', ["GET http://example.com/ HTTP/1.1\r\nHost: \r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('Invalid Host header value', $error->getMessage()); @@ -559,7 +554,7 @@ public function testInvalidConnectRequestWithNonAuthorityForm() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("CONNECT http://example.com:8080/ HTTP/1.1\r\nHost: example.com:8080\r\n\r\n")); + $connection->emit('data', ["CONNECT http://example.com:8080/ HTTP/1.1\r\nHost: example.com:8080\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame('CONNECT method MUST use authority-form request target', $error->getMessage()); @@ -580,7 +575,7 @@ public function testInvalidHttpVersion() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.2\r\n\r\n")); + $connection->emit('data', ["GET / HTTP/1.2\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame(505, $error->getCode()); @@ -602,7 +597,7 @@ public function testInvalidContentLengthRequestHeaderWillEmitError() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: foo\r\n\r\n")); + $connection->emit('data', ["GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: foo\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame(400, $error->getCode()); @@ -624,7 +619,7 @@ public function testInvalidRequestWithMultipleContentLengthRequestHeadersWillEmi $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 4\r\nContent-Length: 5\r\n\r\n")); + $connection->emit('data', ["GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 4\r\nContent-Length: 5\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame(400, $error->getCode()); @@ -646,7 +641,7 @@ public function testInvalidTransferEncodingRequestHeaderWillEmitError() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: foo\r\n\r\n")); + $connection->emit('data', ["GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: foo\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame(501, $error->getCode()); @@ -668,7 +663,7 @@ public function testInvalidRequestWithBothTransferEncodingAndContentLengthWillEm $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\nContent-Length: 0\r\n\r\n")); + $connection->emit('data', ["GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\nContent-Length: 0\r\n\r\n"]); $this->assertInstanceOf('InvalidArgumentException', $error); $this->assertSame(400, $error->getCode()); @@ -688,12 +683,12 @@ public function testServerParamsWillBeSetOnHttpsRequest() $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress', 'getRemoteAddress'))->getMock(); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('tls://127.1.1.1:8000'); $connection->expects($this->once())->method('getRemoteAddress')->willReturn('tls://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")); + $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); $serverParams = $request->getServerParams(); @@ -721,12 +716,12 @@ public function testServerParamsWillBeSetOnHttpRequest() $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress', 'getRemoteAddress'))->getMock(); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['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")); + $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); $serverParams = $request->getServerParams(); @@ -754,12 +749,12 @@ public function testServerParamsWillNotSetRemoteAddressForUnixDomainSockets() $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress', 'getRemoteAddress'))->getMock(); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('unix://./server.sock'); $connection->expects($this->once())->method('getRemoteAddress')->willReturn(null); $parser->handle($connection); - $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); $serverParams = $request->getServerParams(); @@ -790,7 +785,7 @@ public function testServerParamsWontBeSetOnMissingUrls() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); $serverParams = $request->getServerParams(); @@ -811,12 +806,12 @@ public function testServerParamsWillBeReusedForMultipleRequestsFromSameConnectio $parser = new RequestHeaderParser($clock); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress', 'getRemoteAddress'))->getMock(); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['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")); + $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); $request = null; $parser->on('headers', function ($parsedRequest) use (&$request) { @@ -824,7 +819,7 @@ public function testServerParamsWillBeReusedForMultipleRequestsFromSameConnectio }); $parser->handle($connection); - $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); assert($request instanceof ServerRequestInterface); $serverParams = $request->getServerParams(); @@ -846,10 +841,10 @@ public function testServerParamsWillBeRememberedUntilConnectionIsClosed() $parser = new RequestHeaderParser($clock); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('getLocalAddress', 'getRemoteAddress'))->getMock(); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); $ref = new \ReflectionProperty($parser, 'connectionParams'); $ref->setAccessible(true); @@ -857,7 +852,7 @@ public function testServerParamsWillBeRememberedUntilConnectionIsClosed() $this->assertCount(1, $ref->getValue($parser)); $connection->emit('close'); - $this->assertEquals(array(), $ref->getValue($parser)); + $this->assertEquals([], $ref->getValue($parser)); } public function testQueryParmetersWillBeSet() @@ -875,7 +870,7 @@ public function testQueryParmetersWillBeSet() $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $connection->emit('data', array("GET /foo.php?hello=world&test=this HTTP/1.0\r\nHost: example.com\r\n\r\n")); + $connection->emit('data', ["GET /foo.php?hello=world&test=this HTTP/1.0\r\nHost: example.com\r\n\r\n"]); $queryParams = $request->getQueryParams(); diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 03a9b56e..59bb9719 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -9,13 +9,15 @@ use React\Http\Io\ReadableBodyStream; use React\Http\Io\Sender; use React\Http\Message\Request; -use React\Promise; +use React\Promise\Promise; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; +use function React\Promise\reject; +use function React\Promise\resolve; class SenderTest extends TestCase { - /** @var \React\EventLoop\LoopInterface */ + /** @var LoopInterface */ private $loop; /** @@ -55,7 +57,7 @@ public function testSenderRejectsInvalidUri() public function testSenderConnectorRejection() { $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); - $connector->expects($this->once())->method('connect')->willReturn(Promise\reject(new \RuntimeException('Rejected'))); + $connector->expects($this->once())->method('connect')->willReturn(reject(new \RuntimeException('Rejected'))); $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); @@ -80,7 +82,7 @@ public function testSendPostWillAutomaticallySendContentLengthHeader() $sender = new Sender($client); - $request = new Request('POST', 'http://www.google.com/', array(), 'hello'); + $request = new Request('POST', 'http://www.google.com/', [], 'hello'); $sender->send($request); } @@ -93,7 +95,7 @@ public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmpty $sender = new Sender($client); - $request = new Request('POST', 'http://www.google.com/', array(), ''); + $request = new Request('POST', 'http://www.google.com/', [], ''); $sender->send($request); } @@ -110,7 +112,7 @@ public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() $sender = new Sender($client); $stream = new ThroughStream(); - $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://www.google.com/', [], new ReadableBodyStream($stream)); $sender->send($request); } @@ -118,7 +120,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAn { $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); + $outgoing->expects($this->exactly(2))->method('write')->withConsecutive([""], ["5\r\nhello\r\n"])->willReturn(false); $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); $client->expects($this->once())->method('request')->willReturn($outgoing); @@ -126,7 +128,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAn $sender = new Sender($client); $stream = new ThroughStream(); - $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://www.google.com/', [], new ReadableBodyStream($stream)); $sender->send($request); $ret = $stream->write('hello'); @@ -137,7 +139,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() { $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->exactly(2))->method('write')->withConsecutive([""], ["0\r\n\r\n"])->willReturn(false); $outgoing->expects($this->once())->method('end')->with(null); $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); @@ -146,7 +148,7 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() $sender = new Sender($client); $stream = new ThroughStream(); - $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://www.google.com/', [], new ReadableBodyStream($stream)); $sender->send($request); $stream->end(); @@ -167,10 +169,10 @@ public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() $expected = new \RuntimeException(); $stream = new ThroughStream(); - $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://www.google.com/', [], new ReadableBodyStream($stream)); $promise = $sender->send($request); - $stream->emit('error', array($expected)); + $stream->emit('error', [$expected]); $exception = null; $promise->then(null, function ($e) use (&$exception) { @@ -196,7 +198,7 @@ public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() $sender = new Sender($client); $stream = new ThroughStream(); - $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://www.google.com/', [], new ReadableBodyStream($stream)); $promise = $sender->send($request); $stream->close(); @@ -214,7 +216,7 @@ public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() { $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->exactly(2))->method('write')->withConsecutive([""], ["0\r\n\r\n"])->willReturn(false); $outgoing->expects($this->once())->method('end'); $outgoing->expects($this->never())->method('close'); @@ -224,7 +226,7 @@ public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() $sender = new Sender($client); $stream = new ThroughStream(); - $request = new Request('POST', 'http://www.google.com/', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://www.google.com/', [], new ReadableBodyStream($stream)); $promise = $sender->send($request); $stream->end(); @@ -248,7 +250,7 @@ public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() $sender = new Sender($client); $stream = new ThroughStream(); - $request = new Request('POST', 'http://www.google.com/', array('Content-Length' => '100'), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://www.google.com/', ['Content-Length' => '100'], new ReadableBodyStream($stream)); $sender->send($request); } @@ -275,7 +277,7 @@ public function testSendGetWithEmptyBodyStreamWillNotPassContentLengthOrTransfer $sender = new Sender($client); $body = new EmptyBodyStream(); - $request = new Request('GET', 'http://www.google.com/', array(), $body); + $request = new Request('GET', 'http://www.google.com/', [], $body); $sender->send($request); } @@ -302,7 +304,7 @@ public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsI $sender = new Sender($client); - $request = new Request('CUSTOM', 'http://www.google.com/', array('Content-Length' => '0')); + $request = new Request('CUSTOM', 'http://www.google.com/', ['Content-Length' => '0']); $sender->send($request); } @@ -330,13 +332,13 @@ public function getRequestWithUserAndPassShouldSendAGetRequestWithGivenAuthoriza $sender = new Sender($client); - $request = new Request('GET', 'http://john:dummy@www.example.com/', array('Authorization' => 'bearer abc123')); + $request = new Request('GET', 'http://john:dummy@www.example.com/', ['Authorization' => 'bearer abc123']); $sender->send($request); } public function testCancelRequestWillCancelConnector() { - $promise = new \React\Promise\Promise(function () { }, function () { + $promise = new Promise(function () { }, function () { throw new \RuntimeException(); }); @@ -364,7 +366,7 @@ public function testCancelRequestWillCloseConnection() $connection->expects($this->once())->method('close'); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); - $connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($connection)); + $connector->expects($this->once())->method('connect')->willReturn(resolve($connection)); $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 152fece6..3b5f28f4 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -12,6 +12,7 @@ use React\Stream\ThroughStream; use React\Tests\Http\SocketServerStub; use React\Tests\Http\TestCase; +use function React\Promise\resolve; class StreamingServerTest extends TestCase { @@ -37,7 +38,7 @@ private function mockConnection(array $additionalMethods = null) $connection = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() ->setMethods(array_merge( - array( + [ 'write', 'end', 'close', @@ -48,8 +49,8 @@ private function mockConnection(array $additionalMethods = null) 'getRemoteAddress', 'getLocalAddress', 'pipe' - ), - (is_array($additionalMethods) ? $additionalMethods : array()) + ], + (is_array($additionalMethods) ? $additionalMethods : []) )) ->getMock(); @@ -64,11 +65,11 @@ public function testRequestEventWillNotBeEmittedForIncompleteHeaders() $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = ''; $data .= "GET / HTTP/1.1\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestEventIsEmitted() @@ -76,22 +77,22 @@ public function testRequestEventIsEmitted() $server = new StreamingServer(Loop::get(), $this->expectCallableOnce()); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestEventIsEmittedForArrayCallable() { $this->called = null; - $server = new StreamingServer(Loop::get(), array($this, 'helperCallableOnce')); + $server = new StreamingServer(Loop::get(), [$this, 'helperCallableOnce']); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertEquals(1, $this->called); } @@ -116,10 +117,10 @@ public function testRequestEvent() ->willReturn('127.0.0.1:8080'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $serverParams = $requestAssertion->getServerParams(); @@ -128,7 +129,7 @@ public function testRequestEvent() $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); - $this->assertSame(array(), $requestAssertion->getQueryParams()); + $this->assertSame([], $requestAssertion->getQueryParams()); $this->assertSame('http://example.com/', (string)$requestAssertion->getUri()); $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); $this->assertSame('127.0.0.1', $serverParams['REMOTE_ADDR']); @@ -149,10 +150,10 @@ public function testRequestEventWithSingleRequestHandlerArray() ->willReturn('127.0.0.1:8080'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $serverParams = $requestAssertion->getServerParams(); @@ -161,7 +162,7 @@ public function testRequestEventWithSingleRequestHandlerArray() $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); - $this->assertSame(array(), $requestAssertion->getQueryParams()); + $this->assertSame([], $requestAssertion->getQueryParams()); $this->assertSame('http://example.com/', (string)$requestAssertion->getUri()); $this->assertSame('example.com', $requestAssertion->getHeaderLine('Host')); $this->assertSame('127.0.0.1', $serverParams['REMOTE_ADDR']); @@ -175,10 +176,10 @@ public function testRequestGetWithHostAndCustomPort() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: example.com:8080\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); @@ -197,10 +198,10 @@ public function testRequestGetWithHostAndHttpsPort() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: example.com:443\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); @@ -219,10 +220,10 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); @@ -241,10 +242,10 @@ public function testRequestGetHttp10WithoutHostWillBeIgnored() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.0\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); @@ -262,10 +263,10 @@ public function testRequestGetHttp11WithoutHostWillReject() $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestOptionsAsterisk() @@ -276,10 +277,10 @@ public function testRequestOptionsAsterisk() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "OPTIONS * HTTP/1.1\r\nHost: example.com\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('OPTIONS', $requestAssertion->getMethod()); @@ -295,10 +296,10 @@ public function testRequestNonOptionsWithAsteriskRequestTargetWillReject() $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET * HTTP/1.1\r\nHost: example.com\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestConnectAuthorityForm() @@ -309,10 +310,10 @@ public function testRequestConnectAuthorityForm() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); @@ -331,10 +332,10 @@ public function testRequestConnectWithoutHostWillBePassesAsIs() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "CONNECT example.com:443 HTTP/1.1\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); @@ -353,10 +354,10 @@ public function testRequestConnectAuthorityFormWithDefaultPortWillBePassedAsIs() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); @@ -375,10 +376,10 @@ public function testRequestConnectAuthorityFormNonMatchingHostWillBePassedAsIs() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: other.example.org\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); @@ -395,10 +396,10 @@ public function testRequestConnectOriginFormRequestTargetWillReject() $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "CONNECT / HTTP/1.1\r\nHost: example.com\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestNonConnectWithAuthorityRequestTargetWillReject() @@ -407,10 +408,10 @@ public function testRequestNonConnectWithAuthorityRequestTargetWillReject() $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET example.com:80 HTTP/1.1\r\nHost: example.com\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestWithoutHostEventUsesSocketAddress() @@ -427,10 +428,10 @@ public function testRequestWithoutHostEventUsesSocketAddress() ->willReturn('127.0.0.1:80'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET /test HTTP/1.0\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); @@ -448,10 +449,10 @@ public function testRequestAbsoluteEvent() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET http://example.com/test HTTP/1.1\r\nHost: example.com\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); @@ -470,10 +471,10 @@ public function testRequestAbsoluteNonMatchingHostWillBePassedAsIs() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $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->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); @@ -489,10 +490,10 @@ public function testRequestAbsoluteWithoutHostWillReject() $server->on('error', $this->expectCallableOnce()); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET http://example.com:8080/test HTTP/1.1\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestOptionsAsteriskEvent() @@ -504,10 +505,10 @@ public function testRequestOptionsAsteriskEvent() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "OPTIONS * HTTP/1.1\r\nHost: example.com\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('OPTIONS', $requestAssertion->getMethod()); @@ -526,10 +527,10 @@ public function testRequestOptionsAbsoluteEvent() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "OPTIONS http://example.com HTTP/1.1\r\nHost: example.com\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); $this->assertSame('OPTIONS', $requestAssertion->getMethod()); @@ -548,7 +549,7 @@ public function testRequestPauseWillBeForwardedToConnection() $this->connection->expects($this->once())->method('pause'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -556,7 +557,7 @@ public function testRequestPauseWillBeForwardedToConnection() $data .= "Content-Length: 5\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestResumeWillBeForwardedToConnection() @@ -568,7 +569,7 @@ public function testRequestResumeWillBeForwardedToConnection() $this->connection->expects($this->once())->method('resume'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -576,7 +577,7 @@ public function testRequestResumeWillBeForwardedToConnection() $data .= "Content-Length: 5\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestCloseWillNotCloseConnection() @@ -588,10 +589,10 @@ public function testRequestCloseWillNotCloseConnection() $this->connection->expects($this->never())->method('close'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestPauseAfterCloseWillNotBeForwarded() @@ -605,10 +606,10 @@ public function testRequestPauseAfterCloseWillNotBeForwarded() $this->connection->expects($this->never())->method('pause'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestResumeAfterCloseWillNotBeForwarded() @@ -622,10 +623,10 @@ public function testRequestResumeAfterCloseWillNotBeForwarded() $this->connection->expects($this->never())->method('resume'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestEventWithoutBodyWillNotEmitData() @@ -637,10 +638,10 @@ public function testRequestEventWithoutBodyWillNotEmitData() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestEventWithSecondDataEventWillEmitBodyData() @@ -652,7 +653,7 @@ public function testRequestEventWithSecondDataEventWillEmitBodyData() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = ''; $data .= "POST / HTTP/1.1\r\n"; @@ -660,7 +661,7 @@ public function testRequestEventWithSecondDataEventWillEmitBodyData() $data .= "Content-Length: 100\r\n"; $data .= "\r\n"; $data .= "incomplete"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestEventWithPartialBodyWillEmitData() @@ -672,18 +673,18 @@ public function testRequestEventWithPartialBodyWillEmitData() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = ''; $data .= "POST / HTTP/1.1\r\n"; $data .= "Host: localhost\r\n"; $data .= "Content-Length: 100\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $data = ''; $data .= "incomplete"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testResponseContainsServerHeader() @@ -706,10 +707,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("\r\nServer: ReactPHP/1\r\n", $buffer); } @@ -736,10 +737,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertEquals('', $buffer); } @@ -766,10 +767,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->connection->emit('close'); $this->assertEquals('', $buffer); @@ -783,7 +784,7 @@ public function testResponseBodyStreamAlreadyClosedWillSendEmptyBodyChunkedEncod $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array(), + [], $stream ); }); @@ -802,10 +803,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $buffer); $this->assertStringEndsWith("\r\n\r\n0\r\n\r\n", $buffer); @@ -818,7 +819,7 @@ public function testResponseBodyStreamEndingWillSendEmptyBodyChunkedEncoded() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array(), + [], $stream ); }); @@ -837,10 +838,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $stream->end(); @@ -856,7 +857,7 @@ public function testResponseBodyStreamAlreadyClosedWillSendEmptyBodyPlainHttp10( $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array(), + [], $stream ); }); @@ -875,10 +876,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertStringStartsWith("HTTP/1.0 200 OK\r\n", $buffer); $this->assertStringEndsWith("\r\n\r\n", $buffer); @@ -892,7 +893,7 @@ public function testResponseStreamWillBeClosedIfConnectionIsAlreadyClosed() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array(), + [], $stream ); }); @@ -913,7 +914,7 @@ function ($data) use (&$buffer) { $this->connection = $this->getMockBuilder('React\Socket\Connection') ->disableOriginalConstructor() ->setMethods( - array( + [ 'write', 'end', 'close', @@ -924,7 +925,7 @@ function ($data) use (&$buffer) { 'getRemoteAddress', 'getLocalAddress', 'pipe' - ) + ] ) ->getMock(); @@ -933,10 +934,10 @@ function ($data) use (&$buffer) { $this->connection->expects($this->never())->method('write'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testResponseBodyStreamWillBeClosedIfConnectionEmitsCloseEvent() @@ -947,16 +948,16 @@ public function testResponseBodyStreamWillBeClosedIfConnectionEmitsCloseEvent() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array(), + [], $stream ); }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->connection->emit('close'); } @@ -965,11 +966,11 @@ public function testResponseUpgradeInResponseCanBeUsedToAdvertisePossibleUpgrade $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array( + [ 'date' => '', 'server' => '', 'Upgrade' => 'demo' - ), + ], 'foo' ); }); @@ -988,10 +989,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertEquals("HTTP/1.1 200 OK\r\nUpgrade: demo\r\nContent-Length: 3\r\n\r\nfoo", $buffer); } @@ -1001,10 +1002,10 @@ public function testResponseUpgradeWishInRequestCanBeIgnoredByReturningNormalRes $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array( + [ 'date' => '', 'server' => '' - ), + ], 'foo' ); }); @@ -1023,10 +1024,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\nUpgrade: demo\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertEquals("HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nfoo", $buffer); } @@ -1036,11 +1037,11 @@ public function testResponseUpgradeSwitchingProtocolIncludesConnectionUpgradeHea $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 101, - array( + [ 'date' => '', 'server' => '', 'Upgrade' => 'demo' - ), + ], 'foo' ); }); @@ -1061,10 +1062,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\nUpgrade: demo\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertEquals("HTTP/1.1 101 Switching Protocols\r\nUpgrade: demo\r\nConnection: upgrade\r\n\r\nfoo", $buffer); } @@ -1076,11 +1077,11 @@ public function testResponseUpgradeSwitchingProtocolWithStreamWillPipeDataToConn $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 101, - array( + [ 'date' => '', 'server' => '', 'Upgrade' => 'demo' - ), + ], $stream ); }); @@ -1099,10 +1100,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\nUpgrade: demo\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $stream->write('hello'); $stream->write('world'); @@ -1117,7 +1118,7 @@ public function testResponseConnectMethodStreamWillPipeDataToConnection() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array(), + [], $stream ); }); @@ -1136,10 +1137,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $stream->write('hello'); $stream->write('world'); @@ -1155,18 +1156,18 @@ public function testResponseConnectMethodStreamWillPipeDataFromConnection() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array(), + [], $stream ); }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $this->connection->expects($this->once())->method('pipe')->with($stream); $data = "CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForHttp11() @@ -1174,7 +1175,7 @@ public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForH $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array(), + [], 'bye' ); }); @@ -1193,10 +1194,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("bye", $buffer); @@ -1207,7 +1208,7 @@ public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp1 $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array(), + [], 'bye' ); }); @@ -1226,10 +1227,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.0\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.0 200 OK\r\n", $buffer); $this->assertContainsString("\r\n\r\n", $buffer); @@ -1241,7 +1242,7 @@ public function testResponseContainsNoResponseBodyForHeadRequest() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array(), + [], 'bye' ); }); @@ -1259,10 +1260,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); @@ -1277,7 +1278,7 @@ public function testResponseContainsNoResponseBodyForHeadRequestWithStreamingRes $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array('Content-Length' => '3'), + ['Content-Length' => '3'], $stream ); }); @@ -1295,10 +1296,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); @@ -1309,7 +1310,7 @@ public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContent $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 204, - array(), + [], 'bye' ); }); @@ -1327,10 +1328,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 204 No Content\r\n", $buffer); $this->assertNotContainsString("\r\nContent-Length: 3\r\n", $buffer); @@ -1345,7 +1346,7 @@ public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContent $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 204, - array('Content-Length' => '3'), + ['Content-Length' => '3'], $stream ); }); @@ -1363,10 +1364,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 204 No Content\r\n", $buffer); $this->assertNotContainsString("\r\nContent-Length: 3\r\n", $buffer); @@ -1377,7 +1378,7 @@ public function testResponseContainsNoContentLengthHeaderForNotModifiedStatus() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 304, - array(), + [], '' ); }); @@ -1395,10 +1396,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); $this->assertNotContainsString("\r\nContent-Length: 0\r\n", $buffer); @@ -1409,7 +1410,7 @@ public function testResponseContainsExplicitContentLengthHeaderForNotModifiedSta $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 304, - array('Content-Length' => 3), + ['Content-Length' => 3], '' ); }); @@ -1427,10 +1428,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); @@ -1441,7 +1442,7 @@ public function testResponseContainsExplicitContentLengthHeaderForHeadRequests() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array('Content-Length' => 3), + ['Content-Length' => 3], '' ); }); @@ -1459,10 +1460,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); @@ -1473,7 +1474,7 @@ public function testResponseContainsNoResponseBodyForNotModifiedStatus() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 304, - array(), + [], 'bye' ); }); @@ -1491,10 +1492,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); @@ -1509,7 +1510,7 @@ public function testResponseContainsNoResponseBodyForNotModifiedStatusWithStream $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 304, - array('Content-Length' => '3'), + ['Content-Length' => '3'], $stream ); }); @@ -1527,10 +1528,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); @@ -1558,10 +1559,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.2\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('InvalidArgumentException', $error); @@ -1592,11 +1593,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-DATA: "; $data .= str_repeat('A', 8193 - strlen($data)) . "\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('OverflowException', $error); @@ -1626,10 +1627,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "bad request\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('InvalidArgumentException', $error); @@ -1652,7 +1653,7 @@ public function testRequestContentLengthBodyDataWillEmitDataEventOnRequestStream }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1661,7 +1662,7 @@ public function testRequestContentLengthBodyDataWillEmitDataEventOnRequestStream $data .= "\r\n"; $data .= "hello"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestChunkedTransferEncodingRequestWillEmitDecodedDataEventOnRequestStream() @@ -1681,7 +1682,7 @@ public function testRequestChunkedTransferEncodingRequestWillEmitDecodedDataEven }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1691,7 +1692,7 @@ public function testRequestChunkedTransferEncodingRequestWillEmitDecodedDataEven $data .= "5\r\nhello\r\n"; $data .= "0\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertEquals('chunked', $requestValidation->getHeaderLine('Transfer-Encoding')); } @@ -1711,7 +1712,7 @@ public function testRequestChunkedTransferEncodingWithAdditionalDataWontBeEmitte }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1722,7 +1723,7 @@ public function testRequestChunkedTransferEncodingWithAdditionalDataWontBeEmitte $data .= "0\r\n\r\n"; $data .= "2\r\nhi\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestChunkedTransferEncodingEmpty() @@ -1740,7 +1741,7 @@ public function testRequestChunkedTransferEncodingEmpty() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1749,7 +1750,7 @@ public function testRequestChunkedTransferEncodingEmpty() $data .= "\r\n"; $data .= "0\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestChunkedTransferEncodingHeaderCanBeUpperCase() @@ -1769,7 +1770,7 @@ public function testRequestChunkedTransferEncodingHeaderCanBeUpperCase() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1779,7 +1780,7 @@ public function testRequestChunkedTransferEncodingHeaderCanBeUpperCase() $data .= "5\r\nhello\r\n"; $data .= "0\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertEquals('CHUNKED', $requestValidation->getHeaderLine('Transfer-Encoding')); } @@ -1798,7 +1799,7 @@ public function testRequestChunkedTransferEncodingCanBeMixedUpperAndLowerCase() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1807,7 +1808,7 @@ public function testRequestChunkedTransferEncodingCanBeMixedUpperAndLowerCase() $data .= "\r\n"; $data .= "5\r\nhello\r\n"; $data .= "0\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditionalDataWillBeIgnored() @@ -1823,11 +1824,11 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $request->getBody()->on('close', $closeEvent); $request->getBody()->on('error', $errorEvent); - return \React\Promise\resolve(new Response()); + return resolve(new Response()); }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1837,7 +1838,7 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $data .= "hello"; $data .= "world"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditionalDataWillBeIgnoredSplitted() @@ -1856,7 +1857,7 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1865,11 +1866,11 @@ public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditiona $data .= "\r\n"; $data .= "hello"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $data = "world"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestZeroContentLengthWillEmitEndEvent() @@ -1888,7 +1889,7 @@ public function testRequestZeroContentLengthWillEmitEndEvent() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1896,7 +1897,7 @@ public function testRequestZeroContentLengthWillEmitEndEvent() $data .= "Content-Length: 0\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIgnored() @@ -1914,7 +1915,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1923,7 +1924,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $data .= "\r\n"; $data .= "hello"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIgnoredSplitted() @@ -1941,7 +1942,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1949,11 +1950,11 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $data .= "Content-Length: 0\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $data = "hello"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream() @@ -1961,13 +1962,13 @@ public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); - return \React\Promise\resolve(new Response()); + return resolve(new Response()); }); $this->connection->expects($this->never())->method('close'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -1978,7 +1979,7 @@ public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream $data .= 'a'; } - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() @@ -1991,7 +1992,7 @@ public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() $this->connection->expects($this->never())->method('close'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -2000,7 +2001,7 @@ public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() $data .= "\r\n"; $data .= "5\r\nhello world\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWillEmitErrorOnRequestStream() @@ -2013,7 +2014,7 @@ public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWi $this->connection->expects($this->never())->method('close'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -2022,7 +2023,7 @@ public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWi $data .= "\r\n"; $data .= "5\r\nhello\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->connection->emit('end'); } @@ -2036,7 +2037,7 @@ public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() $this->connection->expects($this->never())->method('close'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -2045,7 +2046,7 @@ public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() $data .= "\r\n"; $data .= "hello\r\nhello\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestUnexpectedEndOfRequestWithContentLengthWillEmitErrorOnRequestStream() @@ -2058,7 +2059,7 @@ public function testRequestUnexpectedEndOfRequestWithContentLengthWillEmitErrorO $this->connection->expects($this->never())->method('close'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -2067,7 +2068,7 @@ public function testRequestUnexpectedEndOfRequestWithContentLengthWillEmitErrorO $data .= "\r\n"; $data .= "incomplete"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->connection->emit('end'); } @@ -2088,11 +2089,11 @@ public function testRequestWithoutBodyWillEmitEndOnRequestStream() $this->connection->expects($this->never())->method('close'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() @@ -2110,12 +2111,12 @@ public function testRequestWithoutDefinedLengthWillIgnoreDataEvent() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); $data .= "hello world"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } public function testResponseWithBodyStreamWillUseChunkedTransferEncodingByDefault() @@ -2124,7 +2125,7 @@ public function testResponseWithBodyStreamWillUseChunkedTransferEncodingByDefaul $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array(), + [], $stream ); }); @@ -2142,12 +2143,12 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); - $stream->emit('data', array('hello')); + $this->connection->emit('data', [$data]); + $stream->emit('data', ['hello']); $this->assertContainsString("Transfer-Encoding: chunked", $buffer); $this->assertContainsString("hello", $buffer); @@ -2158,10 +2159,10 @@ public function testResponseWithBodyStringWillOverwriteExplicitContentLengthAndT $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array( + [ 'Content-Length' => 1000, 'Transfer-Encoding' => 'chunked' - ), + ], 'hello' ); }); @@ -2179,11 +2180,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertNotContainsString("Transfer-Encoding: chunked", $buffer); $this->assertContainsString("Content-Length: 5", $buffer); @@ -2199,7 +2200,7 @@ public function testResponseContainsResponseBodyWithTransferEncodingChunkedForBo $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($body) { return new Response( 200, - array(), + [], $body ); }); @@ -2217,10 +2218,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("Transfer-Encoding: chunked", $buffer); $this->assertNotContainsString("Content-Length:", $buffer); @@ -2236,7 +2237,7 @@ public function testResponseContainsResponseBodyWithPlainBodyWithUnknownSizeForL $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($body) { return new Response( 200, - array(), + [], $body ); }); @@ -2254,10 +2255,10 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertNotContainsString("Transfer-Encoding: chunked", $buffer); $this->assertNotContainsString("Content-Length:", $buffer); @@ -2270,9 +2271,9 @@ public function testResponseWithCustomTransferEncodingWillBeIgnoredAndUseChunked $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($stream) { return new Response( 200, - array( + [ 'Transfer-Encoding' => 'custom' - ), + ], $stream ); }); @@ -2290,12 +2291,12 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); - $stream->emit('data', array('hello')); + $this->connection->emit('data', [$data]); + $stream->emit('data', ['hello']); $this->assertContainsString('Transfer-Encoding: chunked', $buffer); $this->assertNotContainsString('Transfer-Encoding: custom', $buffer); @@ -2329,11 +2330,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("Date: Thu, 19 May 2022 14:54:51 GMT\r\n", $buffer); @@ -2345,7 +2346,7 @@ public function testResponseWithCustomDateHeaderOverwritesDefault() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array("Date" => "Tue, 15 Nov 1994 08:12:31 GMT") + ["Date" => "Tue, 15 Nov 1994 08:12:31 GMT"] ); }); @@ -2362,11 +2363,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", $buffer); @@ -2378,7 +2379,7 @@ public function testResponseWithEmptyDateHeaderRemovesDateHeader() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array('Date' => '') + ['Date' => ''] ); }); @@ -2395,11 +2396,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertNotContainsString("Date:", $buffer); @@ -2411,14 +2412,14 @@ public function testResponseCanContainMultipleCookieHeaders() $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array( - 'Set-Cookie' => array( + [ + 'Set-Cookie' => [ 'name=test', 'session=abc' - ), + ], 'Date' => '', 'Server' => '' - ) + ] ); }); @@ -2435,11 +2436,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertEquals("HTTP/1.1 200 OK\r\nSet-Cookie: name=test\r\nSet-Cookie: session=abc\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", $buffer); } @@ -2463,7 +2464,7 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -2471,7 +2472,7 @@ function ($data) use (&$buffer) { $data .= "Expect: 100-continue\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 100 Continue\r\n", $buffer); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); } @@ -2495,13 +2496,13 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.0\r\n"; $data .= "Expect: 100-continue\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.0 200 OK\r\n", $buffer); $this->assertNotContainsString("HTTP/1.1 100 Continue\r\n\r\n", $buffer); } @@ -2519,7 +2520,7 @@ public function testResponseBodyStreamWillStreamDataWithChunkedTransferEncoding( $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($input) { return new Response( 200, - array(), + [], $input ); }); @@ -2537,13 +2538,13 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); - $input->emit('data', array('1')); - $input->emit('data', array('23')); + $this->connection->emit('data', [$data]); + $input->emit('data', ['1']); + $input->emit('data', ['23']); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("\r\n\r\n", $buffer); @@ -2558,7 +2559,7 @@ public function testResponseBodyStreamWithContentLengthWillStreamTillLengthWitho $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) use ($input) { return new Response( 200, - array('Content-Length' => 5), + ['Content-Length' => 5], $input ); }); @@ -2576,13 +2577,13 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); - $input->emit('data', array('hel')); - $input->emit('data', array('lo')); + $this->connection->emit('data', [$data]); + $input->emit('data', ['hel']); + $input->emit('data', ['lo']); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("Content-Length: 5\r\n", $buffer); @@ -2594,7 +2595,7 @@ function ($data) use (&$buffer) { public function testResponseWithResponsePromise() { $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { - return \React\Promise\resolve(new Response()); + return resolve(new Response()); }); $buffer = ''; @@ -2610,11 +2611,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("\r\n\r\n", $buffer); } @@ -2643,11 +2644,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); $this->assertInstanceOf('RuntimeException', $exception); @@ -2656,7 +2657,7 @@ function ($data) use (&$buffer) { public function testResponseResolveWrongTypeInPromiseWillResultInError() { $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { - return \React\Promise\resolve("invalid"); + return resolve("invalid"); }); $buffer = ''; @@ -2672,11 +2673,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } @@ -2703,11 +2704,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } @@ -2734,11 +2735,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } @@ -2748,7 +2749,7 @@ public function testResponseWithContentLengthHeaderForStringBodyOverwritesTransf $server = new StreamingServer(Loop::get(), function (ServerRequestInterface $request) { return new Response( 200, - array('Transfer-Encoding' => 'chunked'), + ['Transfer-Encoding' => 'chunked'], 'hello' ); }); @@ -2766,11 +2767,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); $this->assertContainsString("Content-Length: 5\r\n", $buffer); @@ -2798,11 +2799,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); } @@ -2831,11 +2832,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertInstanceOf('RuntimeException', $exception); $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); @@ -2866,12 +2867,12 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); try { - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); } catch (\Error $e) { $this->markTestSkipped( 'A \Throwable bubbled out of the request callback. ' . @@ -2911,11 +2912,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); $this->assertInstanceOf('RuntimeException', $exception); @@ -2923,49 +2924,49 @@ function ($data) use (&$buffer) { public static function provideInvalidResponse() { - $response = new Response(200, array(), '', '1.1', 'OK'); + $response = new Response(200, [], '', '1.1', 'OK'); - return array( - array( + return [ + [ $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') - ) - ); + ] + ]; } /** @@ -2996,11 +2997,11 @@ function ($data) use (&$buffer) { ); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); $this->assertInstanceOf('InvalidArgumentException', $exception); @@ -3024,11 +3025,11 @@ public function testRequestServerRequestParams() ->willReturn('127.0.0.1:8080'); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = $this->createGetRequest(); - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $serverParams = $requestValidation->getServerParams(); @@ -3048,11 +3049,11 @@ public function testRequestQueryParametersWillBeAddedToRequest() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET /foo.php?hello=world&test=bar HTTP/1.0\r\n\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); $queryParams = $requestValidation->getQueryParams(); @@ -3068,7 +3069,7 @@ public function testRequestCookieWillBeAddedToServerRequest() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -3076,9 +3077,9 @@ public function testRequestCookieWillBeAddedToServerRequest() $data .= "Cookie: hello=world\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); + $this->connection->emit('data', [$data]); - $this->assertEquals(array('hello' => 'world'), $requestValidation->getCookieParams()); + $this->assertEquals(['hello' => 'world'], $requestValidation->getCookieParams()); } public function testRequestInvalidMultipleCookiesWontBeAddedToServerRequest() @@ -3089,7 +3090,7 @@ public function testRequestInvalidMultipleCookiesWontBeAddedToServerRequest() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -3098,8 +3099,8 @@ public function testRequestInvalidMultipleCookiesWontBeAddedToServerRequest() $data .= "Cookie: test=failed\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); - $this->assertEquals(array(), $requestValidation->getCookieParams()); + $this->connection->emit('data', [$data]); + $this->assertEquals([], $requestValidation->getCookieParams()); } public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() @@ -3110,7 +3111,7 @@ public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -3118,8 +3119,8 @@ public function testRequestCookieWithSeparatorWillBeAddedToServerRequest() $data .= "Cookie: hello=world; test=abc\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); - $this->assertEquals(array('hello' => 'world', 'test' => 'abc'), $requestValidation->getCookieParams()); + $this->connection->emit('data', [$data]); + $this->assertEquals(['hello' => 'world', 'test' => 'abc'], $requestValidation->getCookieParams()); } public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() @@ -3130,7 +3131,7 @@ public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() }); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $data = "GET / HTTP/1.1\r\n"; $data .= "Host: example.com\r\n"; @@ -3138,8 +3139,8 @@ public function testRequestCookieWithCommaValueWillBeAddedToServerRequest() $data .= "Cookie: test=abc,def; hello=world\r\n"; $data .= "\r\n"; - $this->connection->emit('data', array($data)); - $this->assertEquals(array('test' => 'abc,def', 'hello' => 'world'), $requestValidation->getCookieParams()); + $this->connection->emit('data', [$data]); + $this->assertEquals(['test' => 'abc,def', 'hello' => 'world'], $requestValidation->getCookieParams()); } public function testNewConnectionWillInvokeParserOnce() @@ -3154,12 +3155,12 @@ public function testNewConnectionWillInvokeParserOnce() $ref->setValue($server, $parser); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); } public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhenParserIsDoneForHttp10() { - $request = new ServerRequest('GET', 'http://localhost/', array(), '', '1.0'); + $request = new ServerRequest('GET', 'http://localhost/', [], '', '1.0'); $server = new StreamingServer(Loop::get(), $this->expectCallableOnceWith($request)); @@ -3171,7 +3172,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen $ref->setValue($server, $parser); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $this->connection->expects($this->once())->method('write'); $this->connection->expects($this->once())->method('end'); @@ -3182,7 +3183,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhenParserIsDoneForHttp11ConnectionClose() { - $request = new ServerRequest('GET', 'http://localhost/', array('Connection' => 'close')); + $request = new ServerRequest('GET', 'http://localhost/', ['Connection' => 'close']); $server = new StreamingServer(Loop::get(), $this->expectCallableOnceWith($request)); @@ -3194,7 +3195,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen $ref->setValue($server, $parser); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $this->connection->expects($this->once())->method('write'); $this->connection->expects($this->once())->method('end'); @@ -3208,7 +3209,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen $request = new ServerRequest('GET', 'http://localhost/'); $server = new StreamingServer(Loop::get(), function () { - return new Response(200, array('Connection' => 'close')); + return new Response(200, ['Connection' => 'close']); }); $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); @@ -3219,7 +3220,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen $ref->setValue($server, $parser); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $this->connection->expects($this->once())->method('write'); $this->connection->expects($this->once())->method('end'); @@ -3244,7 +3245,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle $ref->setValue($server, $parser); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $this->connection->expects($this->once())->method('write'); $this->connection->expects($this->never())->method('end'); @@ -3255,7 +3256,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandlerWhenConnectionCanBeKeptAliveForHttp10ConnectionKeepAlive() { - $request = new ServerRequest('GET', 'http://localhost/', array('Connection' => 'keep-alive'), '', '1.0'); + $request = new ServerRequest('GET', 'http://localhost/', ['Connection' => 'keep-alive'], '', '1.0'); $server = new StreamingServer(Loop::get(), function () { return new Response(); @@ -3269,7 +3270,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle $ref->setValue($server, $parser); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $this->connection->expects($this->once())->method('write'); $this->connection->expects($this->never())->method('end'); @@ -3284,7 +3285,7 @@ public function testNewConnectionWillInvokeParserOnceAfterInvokingRequestHandler $body = new ThroughStream(); $server = new StreamingServer(Loop::get(), function () use ($body) { - return new Response(200, array(), $body); + return new Response(200, [], $body); }); $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); @@ -3295,7 +3296,7 @@ public function testNewConnectionWillInvokeParserOnceAfterInvokingRequestHandler $ref->setValue($server, $parser); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $this->connection->expects($this->once())->method('write'); $this->connection->expects($this->never())->method('end'); @@ -3310,7 +3311,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle $body = new ThroughStream(); $server = new StreamingServer(Loop::get(), function () use ($body) { - return new Response(200, array(), $body); + return new Response(200, [], $body); }); $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); @@ -3321,7 +3322,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle $ref->setValue($server, $parser); $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); + $this->socket->emit('connection', [$this->connection]); $this->connection->expects($this->exactly(2))->method('write'); $this->connection->expects($this->never())->method('end'); @@ -3336,16 +3337,16 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle public function testCompletingARequestWillRemoveConnectionOnCloseListener() { - $connection = $this->mockConnection(array('removeListener')); + $connection = $this->mockConnection(['removeListener']); $request = new ServerRequest('GET', 'http://localhost/'); $server = new StreamingServer(Loop::get(), function () { - return \React\Promise\resolve(new Response()); + return resolve(new Response()); }); $server->listen($this->socket); - $this->socket->emit('connection', array($connection)); + $this->socket->emit('connection', [$connection]); $connection->expects($this->once())->method('removeListener'); diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index 284d059f..b4825024 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -11,10 +11,13 @@ use React\Http\Message\Response; use React\Http\Message\ResponseException; use React\EventLoop\Loop; -use React\Promise; use React\Promise\Deferred; +use React\Promise\Promise; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; +use function React\Async\await; +use function React\Promise\reject; +use function React\Promise\resolve; class TransactionTest extends TestCase { @@ -24,7 +27,7 @@ public function testWithOptionsReturnsNewInstanceWithChangedOption() $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $transaction = new Transaction($sender, $loop); - $new = $transaction->withOptions(array('followRedirects' => false)); + $new = $transaction->withOptions(['followRedirects' => false]); $this->assertInstanceOf('React\Http\Io\Transaction', $new); $this->assertNotSame($transaction, $new); @@ -41,7 +44,7 @@ public function testWithOptionsDoesNotChangeOriginalInstance() $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $transaction = new Transaction($sender, $loop); - $transaction->withOptions(array('followRedirects' => false)); + $transaction->withOptions(['followRedirects' => false]); $ref = new \ReflectionProperty($transaction, 'followRedirects'); $ref->setAccessible(true); @@ -55,8 +58,8 @@ public function testWithOptionsNullValueReturnsNewInstanceWithDefaultOption() $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('followRedirects' => false)); - $transaction = $transaction->withOptions(array('followRedirects' => null)); + $transaction = $transaction->withOptions(['followRedirects' => false]); + $transaction = $transaction->withOptions(['followRedirects' => null]); $ref = new \ReflectionProperty($transaction, 'followRedirects'); $ref->setAccessible(true); @@ -74,10 +77,10 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimer() $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 2)); + $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); @@ -93,7 +96,7 @@ public function testTimeoutImplicitFromIniWillStartTimeoutTimer() $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); @@ -119,10 +122,10 @@ public function testTimeoutExplicitOptionWillRejectWhenTimerFires() $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 2)); + $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); $this->assertNotNull($timeout); @@ -143,13 +146,13 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderResolvesIm $loop->expects($this->never())->method('addTimer'); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = new Response(200, array(), ''); + $response = new Response(200, [], ''); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $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(resolve($response)); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 0.001)); + $transaction = $transaction->withOptions(['timeout' => 0.001]); $promise = $transaction->send($request); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); @@ -164,14 +167,14 @@ public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderResolve $loop->expects($this->once())->method('cancelTimer')->with($timer); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = new Response(200, array(), ''); + $response = new Response(200, [], ''); $deferred = new Deferred(); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 0.001)); + $transaction = $transaction->withOptions(['timeout' => 0.001]); $promise = $transaction->send($request); $deferred->resolve($response); @@ -189,10 +192,10 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderRejectsImm $exception = new \RuntimeException(); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(Promise\reject($exception)); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(reject($exception)); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 0.001)); + $transaction = $transaction->withOptions(['timeout' => 0.001]); $promise = $transaction->send($request); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); @@ -213,7 +216,7 @@ public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderRejects $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 0.001)); + $transaction = $transaction->withOptions(['timeout' => 0.001]); $promise = $transaction->send($request); $exception = new \RuntimeException(); @@ -231,10 +234,10 @@ public function testTimeoutExplicitNegativeWillNotStartTimeoutTimer() $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => -1)); + $transaction = $transaction->withOptions(['timeout' => -1]); $promise = $transaction->send($request); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); @@ -246,13 +249,13 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenRequestBody $loop->expects($this->never())->method('addTimer'); $stream = new ThroughStream(); - $request = new Request('POST', 'http://example.com/', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://example.com/', [], new ReadableBodyStream($stream)); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 2)); + $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); @@ -267,13 +270,13 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingReque $stream = new ThroughStream(); $stream->close(); - $request = new Request('POST', 'http://example.com/', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://example.com/', [], new ReadableBodyStream($stream)); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 2)); + $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); @@ -287,13 +290,13 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingReque $loop->expects($this->never())->method('cancelTimer'); $stream = new ThroughStream(); - $request = new Request('POST', 'http://example.com/', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://example.com/', [], new ReadableBodyStream($stream)); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 2)); + $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); $stream->close(); @@ -307,14 +310,14 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenStreamingRe $loop->expects($this->never())->method('addTimer'); $stream = new ThroughStream(); - $request = new Request('POST', 'http://example.com/', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://example.com/', [], new ReadableBodyStream($stream)); $deferred = new Deferred(); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 2)); + $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); $deferred->reject(new \RuntimeException('Request failed')); @@ -337,13 +340,13 @@ public function testTimeoutExplicitOptionWillRejectWhenTimerFiresAfterStreamingR $loop->expects($this->never())->method('cancelTimer'); $stream = new ThroughStream(); - $request = new Request('POST', 'http://example.com/', array(), new ReadableBodyStream($stream)); + $request = new Request('POST', 'http://example.com/', [], new ReadableBodyStream($stream)); $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new \React\Promise\Promise(function () { })); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 2)); + $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); $stream->close(); @@ -368,10 +371,10 @@ public function testReceivingErrorResponseWillRejectWithResponseException() // 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)); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => -1)); + $transaction = $transaction->withOptions(['timeout' => -1]); $promise = $transaction->send($request); $exception = null; @@ -388,21 +391,21 @@ public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefau { $stream = new ThroughStream(); Loop::addTimer(0.001, function () use ($stream) { - $stream->emit('data', array('hello world')); + $stream->emit('data', ['hello world']); $stream->close(); }); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = new Response(200, array(), new ReadableBodyStream($stream)); + $response = new Response(200, [], 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)); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); $transaction = new Transaction($sender, Loop::get()); $promise = $transaction->send($request); - $response = \React\Async\await($promise); + $response = await($promise); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('hello world', (string)$response->getBody()); @@ -415,11 +418,11 @@ public function testReceivingStreamingBodyWithContentLengthExceedingMaximumRespo $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = new Response(200, array('Content-Length' => '100000000'), new ReadableBodyStream($stream, 100000000)); + $response = new Response(200, ['Content-Length' => '100000000'], new ReadableBodyStream($stream, 100000000)); // 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)); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); $transaction = new Transaction($sender, Loop::get()); @@ -446,14 +449,14 @@ public function testReceivingStreamingBodyWithContentsExceedingMaximumResponseBu $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = new Response(200, array(), new ReadableBodyStream($stream)); + $response = new Response(200, [], 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)); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); $transaction = new Transaction($sender, Loop::get()); - $transaction = $transaction->withOptions(array('maximumSize' => 10)); + $transaction = $transaction->withOptions(['maximumSize' => 10]); $promise = $transaction->send($request); $exception = null; @@ -479,11 +482,11 @@ public function testReceivingStreamingBodyWillRejectWhenStreamEmitsError() }); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = new Response(200, array(), new ReadableBodyStream($stream)); + $response = new Response(200, [], 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)); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); $transaction = new Transaction($sender, Loop::get()); $promise = $transaction->send($request); @@ -511,7 +514,7 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() $stream->expects($this->once())->method('close'); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = new Response(200, array(), new ReadableBodyStream($stream)); + $response = new Response(200, [], new ReadableBodyStream($stream)); // mock sender to resolve promise with the given $response in response to the given $request $deferred = new Deferred(); @@ -541,14 +544,14 @@ public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStre $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = new Response(200, array(), new ReadableBodyStream($this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock())); + $response = new Response(200, [], new ReadableBodyStream($this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock())); // 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)); + $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('streaming' => true, 'timeout' => -1)); + $transaction = $transaction->withOptions(['streaming' => true, 'timeout' => -1]); $promise = $transaction->send($request); $response = null; @@ -566,13 +569,13 @@ public function testResponseCode304WithoutLocationWillResolveWithResponseAsIs() $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); // conditional GET request will respond with 304 (Not Modified - $request = new Request('GET', 'http://example.com/', array('If-None-Match' => '"abc"')); - $response = new Response(304, array('ETag' => '"abc"')); + $request = new Request('GET', 'http://example.com/', ['If-None-Match' => '"abc"']); + $response = new Response(304, ['ETag' => '"abc"']); $sender = $this->makeSenderMock(); - $sender->expects($this->once())->method('send')->with($request)->willReturn(Promise\resolve($response)); + $sender->expects($this->once())->method('send')->with($request)->willReturn(resolve($response)); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => -1)); + $transaction = $transaction->withOptions(['timeout' => -1]); $promise = $transaction->send($request); $promise->then($this->expectCallableOnceWith($response)); @@ -584,16 +587,16 @@ public function testCustomRedirectResponseCode333WillFollowLocationHeaderAndSend // original GET request will respond with custom 333 redirect status code and follow location header $requestOriginal = new Request('GET', 'http://example.com/'); - $response = new Response(333, array('Location' => 'foo')); + $response = new Response(333, ['Location' => 'foo']); $sender = $this->makeSenderMock(); $sender->expects($this->exactly(2))->method('send')->withConsecutive( - array($requestOriginal), - array($this->callback(function (RequestInterface $request) { + [$requestOriginal], + [$this->callback(function (RequestInterface $request) { return $request->getMethod() === 'GET' && (string)$request->getUri() === 'http://example.com/foo'; - })) + })] )->willReturnOnConsecutiveCalls( - Promise\resolve($response), - new \React\Promise\Promise(function () { }) + resolve($response), + new Promise(function () { }) ); $transaction = new Transaction($sender, $loop); @@ -604,27 +607,26 @@ public function testFollowingRedirectWithSpecifiedHeaders() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $customHeaders = array('User-Agent' => 'Chrome'); + $customHeaders = ['User-Agent' => 'Chrome']; $requestWithUserAgent = new Request('GET', 'http://example.com/', $customHeaders); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithUserAgent - $redirectResponse = new Response(301, array('Location' => 'http://redirect.com/')); + $redirectResponse = new Response(301, ['Location' => 'http://redirect.com/']); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithUserAgent $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(array('Chrome'), $request->getHeader('User-Agent')); + [$this->anything()], + [$this->callback(function (RequestInterface $request) { + $this->assertEquals(['Chrome'], $request->getHeader('User-Agent')); return true; - })) + })] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse), - Promise\resolve($okResponse) + resolve($redirectResponse), + resolve($okResponse) ); $transaction = new Transaction($sender, $loop); @@ -635,27 +637,26 @@ public function testRemovingAuthorizationHeaderWhenChangingHostnamesDuringRedire { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $customHeaders = array('Authorization' => 'secret'); + $customHeaders = ['Authorization' => 'secret']; $requestWithAuthorization = new Request('GET', 'http://example.com/', $customHeaders); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithAuthorization - $redirectResponse = new Response(301, array('Location' => 'http://redirect.com/')); + $redirectResponse = new Response(301, ['Location' => 'http://redirect.com/']); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithAuthorization $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->assertFalse($request->hasHeader('Authorization')); + [$this->anything()], + [$this->callback(function (RequestInterface $request) { + $this->assertFalse($request->hasHeader('Authorization')); return true; - })) + })] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse), - Promise\resolve($okResponse) + resolve($redirectResponse), + resolve($okResponse) ); $transaction = new Transaction($sender, $loop); @@ -666,27 +667,26 @@ public function testAuthorizationHeaderIsForwardedWhenRedirectingToSameDomain() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $customHeaders = array('Authorization' => 'secret'); + $customHeaders = ['Authorization' => 'secret']; $requestWithAuthorization = new Request('GET', 'http://example.com/', $customHeaders); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithAuthorization - $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); + $redirectResponse = new Response(301, ['Location' => 'http://example.com/new']); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithAuthorization $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(array('secret'), $request->getHeader('Authorization')); + [$this->anything()], + [$this->callback(function (RequestInterface $request) { + $this->assertEquals(['secret'], $request->getHeader('Authorization')); return true; - })) + })] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse), - Promise\resolve($okResponse) + resolve($redirectResponse), + resolve($okResponse) ); $transaction = new Transaction($sender, $loop); @@ -702,22 +702,21 @@ public function testAuthorizationHeaderIsForwardedWhenLocationContainsAuthentica // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithAuthorization - $redirectResponse = new Response(301, array('Location' => 'http://user:pass@example.com/new')); + $redirectResponse = new Response(301, ['Location' => 'http://user:pass@example.com/new']); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithAuthorization $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('user:pass', $request->getUri()->getUserInfo()); - $that->assertFalse($request->hasHeader('Authorization')); + [$this->anything()], + [$this->callback(function (RequestInterface $request) { + $this->assertEquals('user:pass', $request->getUri()->getUserInfo()); + $this->assertFalse($request->hasHeader('Authorization')); return true; - })) + })] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse), - Promise\resolve($okResponse) + resolve($redirectResponse), + resolve($okResponse) ); $transaction = new Transaction($sender, $loop); @@ -728,32 +727,31 @@ public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $customHeaders = array( + $customHeaders = [ 'Content-Type' => 'text/html; charset=utf-8', - 'Content-Length' => '111', - ); + 'Content-Length' => '111' + ]; $requestWithCustomHeaders = new Request('GET', 'http://example.com/', $customHeaders); $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in // response to the given $requestWithCustomHeaders - $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); + $redirectResponse = new Response(301, ['Location' => 'http://example.com/new']); // mock sender to resolve promise with the given $okResponse in // response to the given $requestWithCustomHeaders $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->assertFalse($request->hasHeader('Content-Type')); - $that->assertFalse($request->hasHeader('Content-Length')); + [$this->anything()], + [$this->callback(function (RequestInterface $request) { + $this->assertFalse($request->hasHeader('Content-Type')); + $this->assertFalse($request->hasHeader('Content-Length')); return true; - })) + })] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse), - Promise\resolve($okResponse) + resolve($redirectResponse), + resolve($okResponse) ); $transaction = new Transaction($sender, $loop); @@ -764,33 +762,32 @@ public function testRequestMethodShouldBeChangedWhenRedirectingWithSeeOther() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $customHeaders = array( + $customHeaders = [ 'Content-Type' => 'text/html; charset=utf-8', - 'Content-Length' => '111', - ); + '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')); + $redirectResponse = new Response(303, ['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')); + [$this->anything()], + [$this->callback(function (RequestInterface $request) { + $this->assertEquals('GET', $request->getMethod()); + $this->assertFalse($request->hasHeader('Content-Type')); + $this->assertFalse($request->hasHeader('Content-Length')); return true; - })) + })] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse), - Promise\resolve($okResponse) + resolve($redirectResponse), + resolve($okResponse) ); $transaction = new Transaction($sender, $loop); @@ -801,40 +798,39 @@ public function testRequestMethodAndBodyShouldNotBeChangedWhenRedirectingWith307 { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $customHeaders = array( + $customHeaders = [ 'Content-Type' => 'text/html; charset=utf-8', - 'Content-Length' => '111', - ); + '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')); + $redirectResponse = new Response(307, ['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') - ), + [$this->anything()], + [$this->callback(function (RequestInterface $request) { + $this->assertEquals('POST', $request->getMethod()); + $this->assertEquals('{"key":"value"}', (string)$request->getBody()); + $this->assertEquals( + [ + 'Content-Type' => ['text/html; charset=utf-8'], + 'Content-Length' => ['111'], + 'Host' => ['example.com'] + ], $request->getHeaders() ); return true; - })) + })] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse), - Promise\resolve($okResponse) + resolve($redirectResponse), + resolve($okResponse) ); $transaction = new Transaction($sender, $loop); @@ -845,10 +841,10 @@ public function testRedirectingStreamingBodyWith307Or308ShouldThrowCantRedirectS { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $customHeaders = array( + $customHeaders = [ 'Content-Type' => 'text/html; charset=utf-8', - 'Content-Length' => '111', - ); + 'Content-Length' => '111' + ]; $stream = new ThroughStream(); $request = new Request('POST', 'http://example.com/', $customHeaders, new ReadableBodyStream($stream)); @@ -856,12 +852,12 @@ public function testRedirectingStreamingBodyWith307Or308ShouldThrowCantRedirectS // 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')); + $redirectResponse = new Response(307, ['Location' => 'http://example.com/new']); $sender->expects($this->once())->method('send')->withConsecutive( - array($this->anything()) + [$this->anything()] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse) + resolve($redirectResponse) ); $transaction = new Transaction($sender, $loop); @@ -883,7 +879,7 @@ public function testCancelTransactionWillCancelRequest() $request = new Request('GET', 'http://example.com/'); $sender = $this->makeSenderMock(); - $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); + $pending = new Promise(function () { }, $this->expectCallableOnce()); // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->once())->method('send')->willReturn($pending); @@ -904,13 +900,13 @@ public function testCancelTransactionWillCancelTimeoutTimer() $request = new Request('GET', 'http://example.com/'); $sender = $this->makeSenderMock(); - $pending = new \React\Promise\Promise(function () { }, function () { throw new \RuntimeException(); }); + $pending = new Promise(function () { }, function () { throw new \RuntimeException(); }); // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->once())->method('send')->willReturn($pending); $transaction = new Transaction($sender, $loop); - $transaction = $transaction->withOptions(array('timeout' => 2)); + $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); $promise->cancel(); @@ -924,16 +920,16 @@ public function testCancelTransactionWillCancelRedirectedRequest() $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in - $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); + $redirectResponse = new Response(301, ['Location' => 'http://example.com/new']); - $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); + $pending = new Promise(function () { }, $this->expectCallableOnce()); // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->exactly(2))->method('send')->withConsecutive( - array($this->anything()), - array($this->anything()) + [$this->anything()], + [$this->anything()] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse), + resolve($redirectResponse), $pending ); @@ -953,12 +949,12 @@ public function testCancelTransactionWillCancelRedirectedRequestAgain() // mock sender to resolve promise with the given $redirectResponse in $first = new Deferred(); - $second = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); + $second = new Promise(function () { }, $this->expectCallableOnce()); // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->exactly(2))->method('send')->withConsecutive( - array($this->anything()), - array($this->anything()) + [$this->anything()], + [$this->anything()] )->willReturnOnConsecutiveCalls( $first->promise(), $second @@ -968,7 +964,7 @@ public function testCancelTransactionWillCancelRedirectedRequestAgain() $promise = $transaction->send($request); // mock sender to resolve promise with the given $redirectResponse in - $first->resolve(new Response(301, array('Location' => 'http://example.com/new'))); + $first->resolve(new Response(301, ['Location' => 'http://example.com/new'])); $promise->cancel(); } @@ -990,7 +986,7 @@ public function testCancelTransactionWillCloseBufferingStream() $transaction = new Transaction($sender, $loop); $promise = $transaction->send($request); - $redirectResponse = new Response(301, array('Location' => 'http://example.com/new'), new ReadableBodyStream($body)); + $redirectResponse = new Response(301, ['Location' => 'http://example.com/new'], new ReadableBodyStream($body)); $deferred->resolve($redirectResponse); $promise->cancel(); @@ -1013,7 +1009,7 @@ public function testCancelTransactionWillCloseBufferingStreamAgain() $body->on('close', $this->expectCallableOnce()); // mock sender to resolve promise with the given $redirectResponse in - $first->resolve(new Response(301, array('Location' => 'http://example.com/new'), new ReadableBodyStream($body))); + $first->resolve(new Response(301, ['Location' => 'http://example.com/new'], new ReadableBodyStream($body))); $promise->cancel(); } @@ -1025,16 +1021,16 @@ public function testCancelTransactionShouldCancelSendingPromise() $sender = $this->makeSenderMock(); // mock sender to resolve promise with the given $redirectResponse in - $redirectResponse = new Response(301, array('Location' => 'http://example.com/new')); + $redirectResponse = new Response(301, ['Location' => 'http://example.com/new']); - $pending = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); + $pending = new Promise(function () { }, $this->expectCallableOnce()); // mock sender to return pending promise which should be cancelled when cancelling result $sender->expects($this->exactly(2))->method('send')->withConsecutive( - array($this->anything()), - array($this->anything()) + [$this->anything()], + [$this->anything()] )->willReturnOnConsecutiveCalls( - Promise\resolve($redirectResponse), + resolve($redirectResponse), $pending ); diff --git a/tests/Io/UploadedFileTest.php b/tests/Io/UploadedFileTest.php index 4e9c0dd5..adbed51c 100644 --- a/tests/Io/UploadedFileTest.php +++ b/tests/Io/UploadedFileTest.php @@ -10,12 +10,12 @@ class UploadedFileTest extends TestCase { public function failtyErrorProvider() { - return array( - array('a'), - array(null), - array(-1), - array(9), - ); + return [ + ['a'], + [null], + [-1], + [9] + ]; } /** diff --git a/tests/Message/RequestTest.php b/tests/Message/RequestTest.php index 29baf8a7..543ddb88 100644 --- a/tests/Message/RequestTest.php +++ b/tests/Message/RequestTest.php @@ -14,7 +14,7 @@ public function testConstructWithStringRequestBodyReturnsStringBodyWithAutomatic $request = new Request( 'GET', 'http://localhost/', - array(), + [], 'foo' ); @@ -28,7 +28,7 @@ public function testConstructWithStreamingRequestBodyReturnsBodyWhichImplementsR $request = new Request( 'GET', 'http://localhost/', - array(), + [], new ThroughStream() ); @@ -43,7 +43,7 @@ public function testConstructWithHttpBodyStreamReturnsBodyAsIs() $request = new Request( 'GET', 'http://localhost/', - array(), + [], $body = new HttpBodyStream(new ThroughStream(), 100) ); @@ -56,7 +56,7 @@ public function testConstructWithNullBodyThrows() new Request( 'GET', 'http://localhost/', - array(), + [], null ); } diff --git a/tests/Message/ResponseTest.php b/tests/Message/ResponseTest.php index 1c70ae3a..61acf19e 100644 --- a/tests/Message/ResponseTest.php +++ b/tests/Message/ResponseTest.php @@ -11,7 +11,7 @@ class ResponseTest extends TestCase { public function testConstructWithStringBodyWillReturnStreamInstance() { - $response = new Response(200, array(), 'hello'); + $response = new Response(200, [], 'hello'); $body = $response->getBody(); /** @var \Psr\Http\Message\StreamInterface $body */ @@ -21,7 +21,7 @@ public function testConstructWithStringBodyWillReturnStreamInstance() public function testConstructWithStreamingBodyWillReturnReadableBodyStream() { - $response = new Response(200, array(), new ThroughStream()); + $response = new Response(200, [], new ThroughStream()); $body = $response->getBody(); /** @var \Psr\Http\Message\StreamInterface $body */ @@ -35,7 +35,7 @@ public function testConstructWithHttpBodyStreamReturnsBodyAsIs() { $response = new Response( 200, - array(), + [], $body = new HttpBodyStream(new ThroughStream(), 100) ); @@ -45,13 +45,13 @@ public function testConstructWithHttpBodyStreamReturnsBodyAsIs() public function testFloatBodyWillThrow() { $this->setExpectedException('InvalidArgumentException'); - new Response(200, array(), 1.0); + new Response(200, [], 1.0); } public function testResourceBodyWillThrow() { $this->setExpectedException('InvalidArgumentException'); - new Response(200, array(), tmpfile()); + new Response(200, [], tmpfile()); } public function testWithStatusReturnsNewInstanceWhenStatusIsChanged() @@ -99,7 +99,7 @@ public function testHtmlMethodReturnsHtmlResponse() public function testJsonMethodReturnsPrettyPrintedJsonResponse() { - $response = Response::json(array('text' => 'Hello wörld!')); + $response = Response::json(['text' => 'Hello wörld!']); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('application/json', $response->getHeaderLine('Content-Type')); @@ -155,7 +155,7 @@ public function testParseMessageWithMinimalOkResponse() $this->assertEquals('1.1', $response->getProtocolVersion()); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('OK', $response->getReasonPhrase()); - $this->assertEquals(array(), $response->getHeaders()); + $this->assertEquals([], $response->getHeaders()); } public function testParseMessageWithSimpleOkResponse() @@ -165,7 +165,7 @@ public function testParseMessageWithSimpleOkResponse() $this->assertEquals('1.1', $response->getProtocolVersion()); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('OK', $response->getReasonPhrase()); - $this->assertEquals(array('Server' => array('demo')), $response->getHeaders()); + $this->assertEquals(['Server' => ['demo']], $response->getHeaders()); } public function testParseMessageWithSimpleOkResponseWithCustomReasonPhrase() @@ -175,7 +175,7 @@ public function testParseMessageWithSimpleOkResponseWithCustomReasonPhrase() $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()); + $this->assertEquals(['Server' => ['demo']], $response->getHeaders()); } public function testParseMessageWithSimpleOkResponseWithEmptyReasonPhraseAppliesDefault() @@ -185,7 +185,7 @@ public function testParseMessageWithSimpleOkResponseWithEmptyReasonPhraseApplies $this->assertEquals('1.1', $response->getProtocolVersion()); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('OK', $response->getReasonPhrase()); - $this->assertEquals(array('Server' => array('demo')), $response->getHeaders()); + $this->assertEquals(['Server' => ['demo']], $response->getHeaders()); } public function testParseMessageWithSimpleOkResponseWithoutReasonPhraseAndWhitespaceSeparatorAppliesDefault() @@ -195,7 +195,7 @@ public function testParseMessageWithSimpleOkResponseWithoutReasonPhraseAndWhites $this->assertEquals('1.1', $response->getProtocolVersion()); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('OK', $response->getReasonPhrase()); - $this->assertEquals(array('Server' => array('demo')), $response->getHeaders()); + $this->assertEquals(['Server' => ['demo']], $response->getHeaders()); } public function testParseMessageWithHttp10SimpleOkResponse() @@ -205,7 +205,7 @@ public function testParseMessageWithHttp10SimpleOkResponse() $this->assertEquals('1.0', $response->getProtocolVersion()); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('OK', $response->getReasonPhrase()); - $this->assertEquals(array('Server' => array('demo')), $response->getHeaders()); + $this->assertEquals(['Server' => ['demo']], $response->getHeaders()); } public function testParseMessageWithHttp10SimpleOkResponseWithLegacyNewlines() @@ -215,7 +215,7 @@ public function testParseMessageWithHttp10SimpleOkResponseWithLegacyNewlines() $this->assertEquals('1.0', $response->getProtocolVersion()); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('OK', $response->getReasonPhrase()); - $this->assertEquals(array('Server' => array('demo')), $response->getHeaders()); + $this->assertEquals(['Server' => ['demo']], $response->getHeaders()); } public function testParseMessageWithInvalidHttpProtocolVersion12Throws() diff --git a/tests/Message/ServerRequestTest.php b/tests/Message/ServerRequestTest.php index f82d60f8..36d20bfa 100644 --- a/tests/Message/ServerRequestTest.php +++ b/tests/Message/ServerRequestTest.php @@ -21,7 +21,7 @@ public function setUpRequest() public function testGetNoAttributes() { - $this->assertEquals(array(), $this->request->getAttributes()); + $this->assertEquals([], $this->request->getAttributes()); } public function testWithAttribute() @@ -29,7 +29,7 @@ public function testWithAttribute() $request = $this->request->withAttribute('hello', 'world'); $this->assertNotSame($request, $this->request); - $this->assertEquals(array('hello' => 'world'), $request->getAttributes()); + $this->assertEquals(['hello' => 'world'], $request->getAttributes()); } public function testGetAttribute() @@ -56,61 +56,61 @@ public function testWithoutAttribute() $request = $request->withoutAttribute('hello'); $this->assertNotSame($request, $this->request); - $this->assertEquals(array('test' => 'nice'), $request->getAttributes()); + $this->assertEquals(['test' => 'nice'], $request->getAttributes()); } public function testGetQueryParamsFromConstructorUri() { $this->request = new ServerRequest('GET', 'http://localhost/?test=world'); - $this->assertEquals(array('test' => 'world'), $this->request->getQueryParams()); + $this->assertEquals(['test' => 'world'], $this->request->getQueryParams()); } public function testWithCookieParams() { - $request = $this->request->withCookieParams(array('test' => 'world')); + $request = $this->request->withCookieParams(['test' => 'world']); $this->assertNotSame($request, $this->request); - $this->assertEquals(array('test' => 'world'), $request->getCookieParams()); + $this->assertEquals(['test' => 'world'], $request->getCookieParams()); } public function testGetQueryParamsFromConstructorUriUrlencoded() { $this->request = new ServerRequest('GET', 'http://localhost/?test=hello+world%21'); - $this->assertEquals(array('test' => 'hello world!'), $this->request->getQueryParams()); + $this->assertEquals(['test' => 'hello world!'], $this->request->getQueryParams()); } public function testWithQueryParams() { - $request = $this->request->withQueryParams(array('test' => 'world')); + $request = $this->request->withQueryParams(['test' => 'world']); $this->assertNotSame($request, $this->request); - $this->assertEquals(array('test' => 'world'), $request->getQueryParams()); + $this->assertEquals(['test' => 'world'], $request->getQueryParams()); } public function testWithQueryParamsWithoutSpecialEncoding() { - $request = $this->request->withQueryParams(array('test' => 'hello world!')); + $request = $this->request->withQueryParams(['test' => 'hello world!']); $this->assertNotSame($request, $this->request); - $this->assertEquals(array('test' => 'hello world!'), $request->getQueryParams()); + $this->assertEquals(['test' => 'hello world!'], $request->getQueryParams()); } public function testWithUploadedFiles() { - $request = $this->request->withUploadedFiles(array('test' => 'world')); + $request = $this->request->withUploadedFiles(['test' => 'world']); $this->assertNotSame($request, $this->request); - $this->assertEquals(array('test' => 'world'), $request->getUploadedFiles()); + $this->assertEquals(['test' => 'world'], $request->getUploadedFiles()); } public function testWithParsedBody() { - $request = $this->request->withParsedBody(array('test' => 'world')); + $request = $this->request->withParsedBody(['test' => 'world']); $this->assertNotSame($request, $this->request); - $this->assertEquals(array('test' => 'world'), $request->getParsedBody()); + $this->assertEquals(['test' => 'world'], $request->getParsedBody()); } public function testServerRequestParameter() @@ -119,10 +119,10 @@ public function testServerRequestParameter() $request = new ServerRequest( 'POST', 'http://127.0.0.1/', - array('Content-Length' => strlen($body)), + ['Content-Length' => strlen($body)], $body, '1.0', - array('SERVER_ADDR' => '127.0.0.1') + ['SERVER_ADDR' => '127.0.0.1'] ); $serverParams = $request->getServerParams(); @@ -139,11 +139,11 @@ public function testParseSingleCookieNameValuePairWillReturnValidArray() $this->request = new ServerRequest( 'GET', 'http://localhost/', - array('Cookie' => 'hello=world') + ['Cookie' => 'hello=world'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array('hello' => 'world'), $cookies); + $this->assertEquals(['hello' => 'world'], $cookies); } public function testParseMultipleCookieNameValuePairWillReturnValidArray() @@ -151,11 +151,11 @@ public function testParseMultipleCookieNameValuePairWillReturnValidArray() $this->request = new ServerRequest( 'GET', 'http://localhost/', - array('Cookie' => 'hello=world; test=abc') + ['Cookie' => 'hello=world; test=abc'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array('hello' => 'world', 'test' => 'abc'), $cookies); + $this->assertEquals(['hello' => 'world', 'test' => 'abc'], $cookies); } public function testParseMultipleCookieHeadersAreNotAllowedAndWillReturnEmptyArray() @@ -163,11 +163,11 @@ public function testParseMultipleCookieHeadersAreNotAllowedAndWillReturnEmptyArr $this->request = new ServerRequest( 'GET', 'http://localhost/', - array('Cookie' => array('hello=world', 'test=abc')) + ['Cookie' => ['hello=world', 'test=abc']] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array(), $cookies); + $this->assertEquals([], $cookies); } public function testMultipleCookiesWithSameNameWillReturnLastValue() @@ -175,11 +175,11 @@ public function testMultipleCookiesWithSameNameWillReturnLastValue() $this->request = new ServerRequest( 'GET', 'http://localhost/', - array('Cookie' => 'hello=world; hello=abc') + ['Cookie' => 'hello=world; hello=abc'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array('hello' => 'abc'), $cookies); + $this->assertEquals(['hello' => 'abc'], $cookies); } public function testOtherEqualSignsWillBeAddedToValueAndWillReturnValidArray() @@ -187,11 +187,11 @@ public function testOtherEqualSignsWillBeAddedToValueAndWillReturnValidArray() $this->request = new ServerRequest( 'GET', 'http://localhost/', - array('Cookie' => 'hello=world=test=php') + ['Cookie' => 'hello=world=test=php'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array('hello' => 'world=test=php'), $cookies); + $this->assertEquals(['hello' => 'world=test=php'], $cookies); } public function testSingleCookieValueInCookiesReturnsEmptyArray() @@ -199,11 +199,11 @@ public function testSingleCookieValueInCookiesReturnsEmptyArray() $this->request = new ServerRequest( 'GET', 'http://localhost/', - array('Cookie' => 'world') + ['Cookie' => 'world'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array(), $cookies); + $this->assertEquals([], $cookies); } public function testSingleMutlipleCookieValuesReturnsEmptyArray() @@ -211,11 +211,11 @@ public function testSingleMutlipleCookieValuesReturnsEmptyArray() $this->request = new ServerRequest( 'GET', 'http://localhost/', - array('Cookie' => 'world; test') + ['Cookie' => 'world; test'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array(), $cookies); + $this->assertEquals([], $cookies); } public function testSingleValueIsValidInMultipleValueCookieWillReturnValidArray() @@ -223,11 +223,11 @@ public function testSingleValueIsValidInMultipleValueCookieWillReturnValidArray( $this->request = new ServerRequest( 'GET', 'http://localhost/', - array('Cookie' => 'world; test=php') + ['Cookie' => 'world; test=php'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array('test' => 'php'), $cookies); + $this->assertEquals(['test' => 'php'], $cookies); } public function testUrlEncodingForValueWillReturnValidArray() @@ -235,11 +235,11 @@ public function testUrlEncodingForValueWillReturnValidArray() $this->request = new ServerRequest( 'GET', 'http://localhost/', - array('Cookie' => 'hello=world%21; test=100%25%20coverage') + ['Cookie' => 'hello=world%21; test=100%25%20coverage'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array('hello' => 'world!', 'test' => '100% coverage'), $cookies); + $this->assertEquals(['hello' => 'world!', 'test' => '100% coverage'], $cookies); } public function testUrlEncodingForKeyWillReturnValidArray() @@ -247,11 +247,11 @@ public function testUrlEncodingForKeyWillReturnValidArray() $this->request = new ServerRequest( 'GET', 'http://localhost/', - array('Cookie' => 'react%3Bphp=is%20great') + ['Cookie' => 'react%3Bphp=is%20great'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array('react%3Bphp' => 'is great'), $cookies); + $this->assertEquals(['react%3Bphp' => 'is great'], $cookies); } public function testCookieWithoutSpaceAfterSeparatorWillBeAccepted() @@ -259,11 +259,11 @@ public function testCookieWithoutSpaceAfterSeparatorWillBeAccepted() $this->request = new ServerRequest( 'GET', 'http://localhost/', - array('Cookie' => 'hello=world;react=php') + ['Cookie' => 'hello=world;react=php'] ); $cookies = $this->request->getCookieParams(); - $this->assertEquals(array('hello' => 'world', 'react' => 'php'), $cookies); + $this->assertEquals(['hello' => 'world', 'react' => 'php'], $cookies); } public function testConstructWithStringRequestBodyReturnsStringBodyWithAutomaticSize() @@ -271,7 +271,7 @@ public function testConstructWithStringRequestBodyReturnsStringBodyWithAutomatic $request = new ServerRequest( 'GET', 'http://localhost/', - array(), + [], 'foo' ); @@ -285,7 +285,7 @@ public function testConstructWithHttpBodyStreamReturnsBodyAsIs() $request = new ServerRequest( 'GET', 'http://localhost/', - array(), + [], $body = new HttpBodyStream(new ThroughStream(), 100) ); @@ -297,7 +297,7 @@ public function testConstructWithStreamingRequestBodyReturnsBodyWhichImplementsR $request = new ServerRequest( 'GET', 'http://localhost/', - array(), + [], new ThroughStream() ); @@ -312,9 +312,9 @@ public function testConstructWithStreamingRequestBodyReturnsBodyWithSizeFromCont $request = new ServerRequest( 'GET', 'http://localhost/', - array( + [ 'Content-Length' => 100 - ), + ], new ThroughStream() ); @@ -329,9 +329,9 @@ public function testConstructWithStreamingRequestBodyReturnsBodyWithSizeUnknownF $request = new ServerRequest( 'GET', 'http://localhost/', - array( + [ 'Transfer-Encoding' => 'Chunked' - ), + ], new ThroughStream() ); @@ -347,7 +347,7 @@ public function testConstructWithFloatRequestBodyThrows() new ServerRequest( 'GET', 'http://localhost/', - array(), + [], 1.0 ); } @@ -358,14 +358,14 @@ public function testConstructWithResourceRequestBodyThrows() new ServerRequest( 'GET', 'http://localhost/', - array(), + [], tmpfile() ); } public function testParseMessageWithSimpleGetRequest() { - $request = ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: example.com\r\n", array()); + $request = ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: example.com\r\n", []); $this->assertEquals('GET', $request->getMethod()); $this->assertEquals('http://example.com/', (string) $request->getUri()); @@ -374,7 +374,7 @@ public function testParseMessageWithSimpleGetRequest() public function testParseMessageWithHttp10RequestWithoutHost() { - $request = ServerRequest::parseMessage("GET / HTTP/1.0\r\n", array()); + $request = ServerRequest::parseMessage("GET / HTTP/1.0\r\n", []); $this->assertEquals('GET', $request->getMethod()); $this->assertEquals('http://127.0.0.1/', (string) $request->getUri()); @@ -383,7 +383,7 @@ public function testParseMessageWithHttp10RequestWithoutHost() public function testParseMessageWithOptionsMethodWithAsteriskFormRequestTarget() { - $request = ServerRequest::parseMessage("OPTIONS * HTTP/1.1\r\nHost: example.com\r\n", array()); + $request = ServerRequest::parseMessage("OPTIONS * HTTP/1.1\r\nHost: example.com\r\n", []); $this->assertEquals('OPTIONS', $request->getMethod()); $this->assertEquals('*', $request->getRequestTarget()); @@ -393,7 +393,7 @@ public function testParseMessageWithOptionsMethodWithAsteriskFormRequestTarget() public function testParseMessageWithConnectMethodWithAuthorityFormRequestTarget() { - $request = ServerRequest::parseMessage("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n", array()); + $request = ServerRequest::parseMessage("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n", []); $this->assertEquals('CONNECT', $request->getMethod()); $this->assertEquals('example.com:80', $request->getRequestTarget()); @@ -404,84 +404,84 @@ public function testParseMessageWithConnectMethodWithAuthorityFormRequestTarget( public function testParseMessageWithInvalidHttp11RequestWithoutHostThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / HTTP/1.1\r\n", array()); + ServerRequest::parseMessage("GET / HTTP/1.1\r\n", []); } public function testParseMessageWithInvalidHttpProtocolVersionThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / HTTP/1.2\r\n", array()); + ServerRequest::parseMessage("GET / HTTP/1.2\r\n", []); } public function testParseMessageWithInvalidProtocolThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / CUSTOM/1.1\r\n", array()); + ServerRequest::parseMessage("GET / CUSTOM/1.1\r\n", []); } public function testParseMessageWithInvalidHostHeaderWithoutValueThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost\r\n", array()); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost\r\n", []); } public function testParseMessageWithInvalidHostHeaderSyntaxThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: ///\r\n", array()); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: ///\r\n", []); } public function testParseMessageWithInvalidHostHeaderWithSchemeThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: http://localhost\r\n", array()); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: http://localhost\r\n", []); } public function testParseMessageWithInvalidHostHeaderWithQueryThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost?foo\r\n", array()); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost?foo\r\n", []); } public function testParseMessageWithInvalidHostHeaderWithFragmentThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost#foo\r\n", array()); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost#foo\r\n", []); } public function testParseMessageWithInvalidContentLengthHeaderThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length:\r\n", array()); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length:\r\n", []); } public function testParseMessageWithInvalidTransferEncodingHeaderThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding:\r\n", array()); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding:\r\n", []); } 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()); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\nTransfer-Encoding: chunked\r\n", []); } public function testParseMessageWithInvalidEmptyHostHeaderWithAbsoluteFormRequestTargetThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET http://example.com/ HTTP/1.1\r\nHost: \r\n", array()); + ServerRequest::parseMessage("GET http://example.com/ HTTP/1.1\r\nHost: \r\n", []); } public function testParseMessageWithInvalidConnectMethodNotUsingAuthorityFormThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("CONNECT / HTTP/1.1\r\nHost: localhost\r\n", array()); + ServerRequest::parseMessage("CONNECT / HTTP/1.1\r\nHost: localhost\r\n", []); } public function testParseMessageWithInvalidRequestTargetAsteriskFormThrows() { $this->setExpectedException('InvalidArgumentException'); - ServerRequest::parseMessage("GET * HTTP/1.1\r\nHost: localhost\r\n", array()); + ServerRequest::parseMessage("GET * HTTP/1.1\r\nHost: localhost\r\n", []); } } diff --git a/tests/Message/UriTest.php b/tests/Message/UriTest.php index 10f355df..cdbc5a87 100644 --- a/tests/Message/UriTest.php +++ b/tests/Message/UriTest.php @@ -33,95 +33,95 @@ public function testCtorWithInvalidPortThrows() public static function provideValidUris() { - return array( - array( + return [ + [ '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' - ) - ); + ] + ]; } /** @@ -137,36 +137,36 @@ public function testToStringReturnsOriginalUriGivenToCtor($string) public static function provideValidUrisThatWillBeTransformed() { - return array( - array( + return [ + [ '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' - ) - ); + ] + ]; } /** @@ -576,113 +576,113 @@ public function testWithFragmentReturnsSameInstanceWhenFragmentIsUnchangedEncode public static function provideResolveUris() { - return array( - array( + return [ + [ '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/' - ) - ); + ] + ]; } /** diff --git a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php index 23455e6c..b79826d6 100644 --- a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php +++ b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php @@ -163,7 +163,7 @@ public function testStreamDoesNotPauseOrResumeWhenBelowLimit() $body->expects($this->never())->method('pause'); $body->expects($this->never())->method('resume'); $limitHandlers = new LimitConcurrentRequestsMiddleware(1); - $limitHandlers(new ServerRequest('GET', 'https://example.com/', array(), $body), function () {}); + $limitHandlers(new ServerRequest('GET', 'https://example.com/', [], $body), function () {}); } public function testStreamDoesPauseWhenAboveLimit() @@ -177,7 +177,7 @@ public function testStreamDoesPauseWhenAboveLimit() return new Promise(function () { }); }); - $limitHandlers(new ServerRequest('GET', 'https://example.com/', array(), $body), function () {}); + $limitHandlers(new ServerRequest('GET', 'https://example.com/', [], $body), function () {}); } public function testStreamDoesPauseAndThenResumeWhenDequeued() @@ -195,7 +195,7 @@ public function testStreamDoesPauseAndThenResumeWhenDequeued() assert($promise instanceof PromiseInterface); $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection - $limitHandlers(new ServerRequest('GET', 'https://example.com/', array(), $body), function () {}); + $limitHandlers(new ServerRequest('GET', 'https://example.com/', [], $body), function () {}); $deferred->reject(new \RuntimeException()); } @@ -205,7 +205,7 @@ public function testReceivesBufferedRequestSameInstance() $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -224,7 +224,7 @@ public function testReceivesStreamingBodyRequestSameInstanceWhenBelowLimit() $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], new HttpBodyStream($stream, 5) ); @@ -246,7 +246,7 @@ public function testReceivesRequestsSequentially() $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -261,7 +261,7 @@ public function testDoesNotReceiveNextRequestIfHandlerIsPending() $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -280,7 +280,7 @@ public function testReceivesNextRequestAfterPreviousHandlerIsSettled() $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -303,7 +303,7 @@ public function testReceivesNextRequestWhichThrowsAfterPreviousHandlerIsSettled( $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -331,7 +331,7 @@ public function testPendingRequestCanBeCancelledAndForwardsCancellationToInnerPr $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -354,7 +354,7 @@ public function testQueuedRequestCanBeCancelledBeforeItStartsProcessing() $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -376,7 +376,7 @@ public function testReceivesNextRequestAfterPreviousHandlerIsCancelled() $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -400,7 +400,7 @@ public function testRejectsWhenQueuedPromiseIsCancelled() $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -422,7 +422,7 @@ public function testDoesNotInvokeNextHandlersWhenQueuedPromiseIsCancelled() $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], 'hello' ); @@ -445,7 +445,7 @@ public function testReceivesStreamingBodyChangesInstanceWithCustomBodyButSameDat $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], new HttpBodyStream($stream, 5) ); @@ -494,7 +494,7 @@ public function testReceivesNextStreamingBodyWithBufferedDataAfterPreviousHandle $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], new HttpBodyStream($stream, 10) ); @@ -524,7 +524,7 @@ public function testReceivesNextStreamingBodyAndDoesNotEmitDataIfExplicitlyClose $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], new HttpBodyStream($stream, 10) ); @@ -555,7 +555,7 @@ public function testReceivesNextStreamingBodyAndDoesNotEmitDataIfExplicitlyPause $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], new HttpBodyStream($stream, 10) ); @@ -586,7 +586,7 @@ public function testReceivesNextStreamingBodyAndDoesEmitDataImmediatelyIfExplici $request = new ServerRequest( 'POST', 'http://example.com/', - array(), + [], new HttpBodyStream($stream, 10) ); diff --git a/tests/Middleware/ProcessStack.php b/tests/Middleware/ProcessStack.php index 69bf34a8..22904310 100644 --- a/tests/Middleware/ProcessStack.php +++ b/tests/Middleware/ProcessStack.php @@ -3,7 +3,7 @@ namespace React\Tests\Http\Middleware; use Psr\Http\Message\ServerRequestInterface; -use React\Promise; +use function React\Promise\resolve; final class ProcessStack { @@ -15,7 +15,7 @@ final class ProcessStack public function __invoke(ServerRequestInterface $request, $stack) { $this->callCount++; - return Promise\resolve($stack($request)); + return resolve($stack($request)); } /** diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index 1c3b0b33..262ad9ca 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -11,6 +11,7 @@ use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; +use function React\Async\await; final class RequestBodyBufferMiddlewareTest extends TestCase { @@ -20,7 +21,7 @@ public function testBufferingResolvesWhenStreamEnds() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, 11) ); @@ -49,7 +50,7 @@ public function testAlreadyBufferedResolvesImmediately() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], $stream ); @@ -72,7 +73,7 @@ public function testEmptyStreamingResolvesImmediatelyWithEmptyBufferedBody() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], $body = new HttpBodyStream($stream, 0) ); @@ -95,7 +96,7 @@ public function testEmptyBufferedResolvesImmediatelyWithSameBody() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], '' ); $body = $serverRequest->getBody(); @@ -122,7 +123,7 @@ public function testClosedStreamResolvesImmediatelyWithEmptyBody() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, 2) ); @@ -145,7 +146,7 @@ public function testKnownExcessiveSizedBodyIsDiscardedAndRequestIsPassedDownToTh $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, 2) ); @@ -154,13 +155,13 @@ public function testKnownExcessiveSizedBodyIsDiscardedAndRequestIsPassedDownToTh $promise = $buffer( $serverRequest, function (ServerRequestInterface $request) { - return new Response(200, array(), $request->getBody()->getContents()); + return new Response(200, [], $request->getBody()->getContents()); } ); $stream->end('aa'); - $response = \React\Async\await($promise); + $response = await($promise); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('', $response->getBody()->getContents()); @@ -175,15 +176,15 @@ public function testKnownExcessiveSizedWithIniLikeSize() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, 2048) ); $buffer = new RequestBodyBufferMiddleware('1K'); - $response = \React\Async\await($buffer( + $response = await($buffer( $serverRequest, function (ServerRequestInterface $request) { - return new Response(200, array(), $request->getBody()->getContents()); + return new Response(200, [], $request->getBody()->getContents()); } )); @@ -196,7 +197,7 @@ public function testAlreadyBufferedExceedingSizeResolvesImmediatelyWithEmptyBody $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], 'hello' ); @@ -219,7 +220,7 @@ public function testExcessiveSizeBodyIsDiscardedAndTheRequestIsPassedDownToTheNe $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, null) ); @@ -227,13 +228,13 @@ public function testExcessiveSizeBodyIsDiscardedAndTheRequestIsPassedDownToTheNe $promise = $buffer( $serverRequest, function (ServerRequestInterface $request) { - return new Response(200, array(), $request->getBody()->getContents()); + return new Response(200, [], $request->getBody()->getContents()); } ); $stream->end('aa'); - $exposedResponse = \React\Async\await($promise->then( + $exposedResponse = await($promise->then( null, $this->expectCallableNever() )); @@ -249,7 +250,7 @@ public function testBufferingRejectsWhenNextHandlerThrowsWhenStreamEnds() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, null) ); @@ -284,7 +285,7 @@ public function testBufferingRejectsWhenNextHandlerThrowsErrorWhenStreamEnds() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, null) ); @@ -321,7 +322,7 @@ public function testBufferingRejectsWhenStreamEmitsError() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, null) ); @@ -355,7 +356,7 @@ public function testFullBodyStreamedBeforeCallingNextMiddleware() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, null) ); @@ -381,7 +382,7 @@ public function testCancelBufferingClosesStreamAndRejectsPromise() $serverRequest = new ServerRequest( 'GET', 'https://example.com/', - array(), + [], new HttpBodyStream($stream, 2) ); diff --git a/tests/Middleware/RequestBodyParserMiddlewareTest.php b/tests/Middleware/RequestBodyParserMiddlewareTest.php index b588bdd5..b601b478 100644 --- a/tests/Middleware/RequestBodyParserMiddlewareTest.php +++ b/tests/Middleware/RequestBodyParserMiddlewareTest.php @@ -15,9 +15,9 @@ public function testFormUrlencodedParsing() $request = new ServerRequest( 'POST', 'https://example.com/', - array( - 'Content-Type' => 'application/x-www-form-urlencoded', - ), + [ + 'Content-Type' => 'application/x-www-form-urlencoded' + ], 'hello=world' ); @@ -30,7 +30,7 @@ function (ServerRequestInterface $request) { ); $this->assertSame( - array('hello' => 'world'), + ['hello' => 'world'], $parsedRequest->getParsedBody() ); $this->assertSame('hello=world', (string)$parsedRequest->getBody()); @@ -42,9 +42,9 @@ public function testFormUrlencodedParsingIgnoresCaseForHeadersButRespectsContent $request = new ServerRequest( 'POST', 'https://example.com/', - array( - 'CONTENT-TYPE' => 'APPLICATION/X-WWW-Form-URLEncoded', - ), + [ + 'CONTENT-TYPE' => 'APPLICATION/X-WWW-Form-URLEncoded' + ], 'Hello=World' ); @@ -57,7 +57,7 @@ function (ServerRequestInterface $request) { ); $this->assertSame( - array('Hello' => 'World'), + ['Hello' => 'World'], $parsedRequest->getParsedBody() ); $this->assertSame('Hello=World', (string)$parsedRequest->getBody()); @@ -69,9 +69,9 @@ public function testFormUrlencodedParsingNestedStructure() $request = new ServerRequest( 'POST', 'https://example.com/', - array( - 'Content-Type' => 'application/x-www-form-urlencoded', - ), + [ + 'Content-Type' => 'application/x-www-form-urlencoded' + ], 'foo=bar&baz[]=cheese&bar[]=beer&bar[]=wine&market[fish]=salmon&market[meat][]=beef&market[meat][]=chicken&market[]=bazaar' ); @@ -84,24 +84,24 @@ function (ServerRequestInterface $request) { ); $this->assertSame( - array( + [ 'foo' => 'bar', - 'baz' => array( + 'baz' => [ 'cheese', - ), - 'bar' => array( + ], + 'bar' => [ 'beer', 'wine', - ), - 'market' => array( + ], + 'market' => [ 'fish' => 'salmon', - 'meat' => array( + 'meat' => [ 'beef', 'chicken', - ), + ], 0 => 'bazaar', - ), - ), + ], + ], $parsedRequest->getParsedBody() ); $this->assertSame('foo=bar&baz[]=cheese&bar[]=beer&bar[]=wine&market[fish]=salmon&market[meat][]=beef&market[meat][]=chicken&market[]=bazaar', (string)$parsedRequest->getBody()); @@ -115,9 +115,9 @@ public function testFormUrlencodedIgnoresBodyWithExcessiveNesting() $request = new ServerRequest( 'POST', 'https://example.com/', - array( - 'Content-Type' => 'application/x-www-form-urlencoded', - ), + [ + 'Content-Type' => 'application/x-www-form-urlencoded' + ], 'hello' . str_repeat('[]', $allowed + 1) . '=world' ); @@ -130,7 +130,7 @@ function (ServerRequestInterface $request) { ); $this->assertSame( - array(), + [], $parsedRequest->getParsedBody() ); } @@ -143,9 +143,9 @@ public function testFormUrlencodedTruncatesBodyWithExcessiveLength() $request = new ServerRequest( 'POST', 'https://example.com/', - array( - 'Content-Type' => 'application/x-www-form-urlencoded', - ), + [ + 'Content-Type' => 'application/x-www-form-urlencoded' + ], str_repeat('a[]=b&', $allowed + 1) ); @@ -170,9 +170,9 @@ public function testDoesNotParseJsonByDefault() $request = new ServerRequest( 'POST', 'https://example.com/', - array( - 'Content-Type' => 'application/json', - ), + [ + 'Content-Type' => 'application/json' + ], '{"hello":"world"}' ); @@ -204,9 +204,9 @@ public function testMultipartFormDataParsing() $data .= "second\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + $request = new ServerRequest('POST', 'http://example.com/', [ + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary + ], $data, 1.1); /** @var ServerRequestInterface $parsedRequest */ $parsedRequest = $middleware( @@ -217,12 +217,12 @@ function (ServerRequestInterface $request) { ); $this->assertSame( - array( - 'users' => array( + [ + 'users' => [ 'one' => 'single', 'two' => 'second', - ), - ), + ], + ], $parsedRequest->getParsedBody() ); $this->assertSame($data, (string)$parsedRequest->getBody()); @@ -242,9 +242,9 @@ public function testMultipartFormDataIgnoresFieldWithExcessiveNesting() $data .= "world\r\n"; $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + $request = new ServerRequest('POST', 'http://example.com/', [ + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary + ], $data, 1.1); /** @var ServerRequestInterface $parsedRequest */ $parsedRequest = $middleware( @@ -274,9 +274,9 @@ public function testMultipartFormDataTruncatesBodyWithExcessiveLength() } $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + $request = new ServerRequest('POST', 'http://example.com/', [ + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary + ], $data, 1.1); /** @var ServerRequestInterface $parsedRequest */ $parsedRequest = $middleware( @@ -310,9 +310,9 @@ public function testMultipartFormDataTruncatesExcessiveNumberOfEmptyFileUploads( } $data .= "--$boundary--\r\n"; - $request = new ServerRequest('POST', 'http://example.com/', array( - 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, - ), $data, 1.1); + $request = new ServerRequest('POST', 'http://example.com/', [ + 'Content-Type' => 'multipart/form-data; boundary=' . $boundary + ], $data, 1.1); /** @var ServerRequestInterface $parsedRequest */ $parsedRequest = $middleware( diff --git a/tests/TestCase.php b/tests/TestCase.php index 88d8a3df..fa6fcd1c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,7 +6,7 @@ class TestCase extends BaseTestCase { - public function expectCallableOnce() // protected (PHP 5.4+) + protected function expectCallableOnce() { $mock = $this->createCallableMock(); $mock @@ -16,7 +16,7 @@ public function expectCallableOnce() // protected (PHP 5.4+) return $mock; } - public function expectCallableOnceWith($value) // protected (PHP 5.4+) + protected function expectCallableOnceWith($value) { $mock = $this->createCallableMock(); $mock @@ -27,7 +27,7 @@ public function expectCallableOnceWith($value) // protected (PHP 5.4+) return $mock; } - public function expectCallableNever() // protected (PHP 5.4+) + protected function expectCallableNever() { $mock = $this->createCallableMock(); $mock @@ -41,10 +41,10 @@ protected function createCallableMock() { if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { // PHPUnit 9+ - return $this->getMockBuilder('stdClass')->addMethods(array('__invoke'))->getMock(); + return $this->getMockBuilder('stdClass')->addMethods(['__invoke'])->getMock(); } else { // legacy PHPUnit 4 - PHPUnit 8 - return $this->getMockBuilder('stdClass')->setMethods(array('__invoke'))->getMock(); + return $this->getMockBuilder('stdClass')->setMethods(['__invoke'])->getMock(); } } diff --git a/tests/benchmark-middleware-runner.php b/tests/benchmark-middleware-runner.php index 3f1dacaf..d330a1b0 100644 --- a/tests/benchmark-middleware-runner.php +++ b/tests/benchmark-middleware-runner.php @@ -13,7 +13,7 @@ $middleware = function (ServerRequestInterface $request, $next) { return $next($request); }; -$middlewareList = array(); +$middlewareList = []; for ($i = 0; $i < MIDDLEWARE_COUNT; $i++) { $middlewareList[] = $middleware; } From 012ee77f237d62431e90e9c531c8b01200025bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 26 May 2024 20:17:32 +0200 Subject: [PATCH 148/152] Update test suite and remove legacy PHPUnit workarounds --- composer.json | 2 +- phpunit.xml.legacy | 2 +- tests/BrowserTest.php | 215 ++--- tests/FunctionalBrowserTest.php | 46 +- tests/FunctionalHttpServerTest.php | 52 +- tests/HttpServerTest.php | 50 +- tests/Io/AbstractMessageTest.php | 18 +- tests/Io/AbstractRequestTest.php | 64 +- tests/Io/BufferedBodyTest.php | 18 +- tests/Io/ChunkedDecoderTest.php | 8 +- tests/Io/ChunkedEncoderTest.php | 8 +- tests/Io/ClientConnectionManagerTest.php | 94 +- tests/Io/ClientRequestStreamTest.php | 250 +++--- tests/Io/ClockTest.php | 5 +- tests/Io/CloseProtectionStreamTest.php | 14 +- tests/Io/EmptyBodyStreamTest.php | 15 +- tests/Io/HttpBodyStreamTest.php | 20 +- tests/Io/IniUtilTest.php | 84 +- tests/Io/LengthLimitedStreamTest.php | 8 +- tests/Io/MiddlewareRunnerTest.php | 329 ++++--- tests/Io/MultipartParserTest.php | 15 +- tests/Io/PauseBufferStreamTest.php | 9 +- tests/Io/ReadableBodyStreamTest.php | 21 +- tests/Io/RequestHeaderParserTest.php | 198 +++-- tests/Io/SenderTest.php | 90 +- tests/Io/StreamingServerTest.php | 835 ++++++++---------- tests/Io/TransactionTest.php | 233 ++--- tests/Io/UploadedFileTest.php | 21 +- tests/Message/RequestTest.php | 9 +- tests/Message/ResponseTest.php | 25 +- tests/Message/ServerRequestTest.php | 46 +- tests/Message/UriTest.php | 458 +++++----- .../LimitConcurrentRequestsMiddlewareTest.php | 19 +- .../RequestBodyBufferMiddlewareTest.php | 10 +- tests/TestCase.php | 43 +- 35 files changed, 1618 insertions(+), 1716 deletions(-) diff --git a/composer.json b/composer.json index 33919186..2fe67da0 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ "clue/http-proxy-react": "^1.8", "clue/reactphp-ssh-proxy": "^1.4", "clue/socks-react": "^1.4", - "phpunit/phpunit": "^9.6 || ^5.7", + "phpunit/phpunit": "^9.6 || ^7.5", "react/async": "^4 || ^3", "react/promise-stream": "^1.4", "react/promise-timer": "^1.9" diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy index a018d7ab..00868603 100644 --- a/phpunit.xml.legacy +++ b/phpunit.xml.legacy @@ -2,7 +2,7 @@ diff --git a/tests/BrowserTest.php b/tests/BrowserTest.php index a7188b2c..8f3e10bd 100644 --- a/tests/BrowserTest.php +++ b/tests/BrowserTest.php @@ -3,8 +3,11 @@ namespace React\Tests\Http; use Psr\Http\Message\RequestInterface; +use React\EventLoop\LoopInterface; +use React\Http\Io\Transaction; use React\Http\Browser; use React\Promise\Promise; +use React\Socket\ConnectorInterface; class BrowserTest extends TestCase { @@ -17,8 +20,8 @@ class BrowserTest extends TestCase */ public function setUpBrowser() { - $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $this->sender = $this->getMockBuilder('React\Http\Io\Transaction')->disableOriginalConstructor()->getMock(); + $this->loop = $this->createMock(LoopInterface::class); + $this->sender = $this->createMock(Transaction::class); $this->browser = new Browser(null, $this->loop); $ref = new \ReflectionProperty($this->browser, 'transaction'); @@ -38,12 +41,12 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() $ref->setAccessible(true); $loop = $ref->getValue($transaction); - $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + $this->assertInstanceOf(LoopInterface::class, $loop); } public function testConstructWithConnectorAssignsGivenConnector() { - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $browser = new Browser($connector); @@ -250,108 +253,106 @@ public function testWithBase() { $browser = $this->browser->withBase('http://example.com/root'); - $this->assertInstanceOf('React\Http\Browser', $browser); + $this->assertInstanceOf(Browser::class, $browser); $this->assertNotSame($this->browser, $browser); } - public function provideOtherUris() - { - return [ - 'empty returns base' => [ - 'http://example.com/base', - '', - 'http://example.com/base', - ], - 'absolute same as base returns base' => [ - 'http://example.com/base', - 'http://example.com/base', - 'http://example.com/base', - ], - 'absolute below base returns absolute' => [ - 'http://example.com/base', - 'http://example.com/base/another', - 'http://example.com/base/another', - ], - 'slash returns base without path' => [ - 'http://example.com/base', - '/', - 'http://example.com/', - ], - 'relative is added behind base' => [ - 'http://example.com/base/', - 'test', - 'http://example.com/base/test', - ], - 'relative is added behind base without path' => [ - 'http://example.com/base', - 'test', - 'http://example.com/test', - ], - 'relative level up is added behind parent path' => [ - 'http://example.com/base/foo/', - '../bar', - 'http://example.com/base/bar', - ], - 'absolute with slash is added behind base without path' => [ - 'http://example.com/base', - '/test', - 'http://example.com/test', - ], - 'query string is added behind base' => [ - 'http://example.com/base', - '?key=value', - 'http://example.com/base?key=value', - ], - 'query string is added behind base with slash' => [ - 'http://example.com/base/', - '?key=value', - 'http://example.com/base/?key=value', - ], - 'query string with slash is added behind base without path' => [ - 'http://example.com/base', - '/?key=value', - 'http://example.com/?key=value', - ], - 'absolute with query string below base is returned as-is' => [ - 'http://example.com/base', - 'http://example.com/base?test', - 'http://example.com/base?test', - ], - 'urlencoded special chars will stay as-is' => [ - 'http://example.com/%7Bversion%7D/', - '', - 'http://example.com/%7Bversion%7D/' - ], - 'special chars will be urlencoded' => [ - 'http://example.com/%7Bversion%7D/', - '', - 'http://example.com/%7Bversion%7D/' - ], - 'other domain' => [ - 'http://example.com/base/', - 'http://example.org/base/', - 'http://example.org/base/' - ], - 'other scheme' => [ - 'http://example.com/base/', - 'https://example.com/base/', - 'https://example.com/base/' - ], - 'other port' => [ - 'http://example.com/base/', - 'http://example.com:81/base/', - 'http://example.com:81/base/' - ], - 'other path' => [ - 'http://example.com/base/', - 'http://example.com/other/', - 'http://example.com/other/' - ], - 'other path due to missing slash' => [ - 'http://example.com/base/', - 'http://example.com/other', - 'http://example.com/other' - ], + public static function provideOtherUris() + { + yield 'empty returns base' => [ + 'http://example.com/base', + '', + 'http://example.com/base', + ]; + yield 'absolute same as base returns base' => [ + 'http://example.com/base', + 'http://example.com/base', + 'http://example.com/base', + ]; + yield 'absolute below base returns absolute' => [ + 'http://example.com/base', + 'http://example.com/base/another', + 'http://example.com/base/another', + ]; + yield 'slash returns base without path' => [ + 'http://example.com/base', + '/', + 'http://example.com/', + ]; + yield 'relative is added behind base' => [ + 'http://example.com/base/', + 'test', + 'http://example.com/base/test', + ]; + yield 'relative is added behind base without path' => [ + 'http://example.com/base', + 'test', + 'http://example.com/test', + ]; + yield 'relative level up is added behind parent path' => [ + 'http://example.com/base/foo/', + '../bar', + 'http://example.com/base/bar', + ]; + yield 'absolute with slash is added behind base without path' => [ + 'http://example.com/base', + '/test', + 'http://example.com/test', + ]; + yield 'query string is added behind base' => [ + 'http://example.com/base', + '?key=value', + 'http://example.com/base?key=value', + ]; + yield 'query string is added behind base with slash' => [ + 'http://example.com/base/', + '?key=value', + 'http://example.com/base/?key=value', + ]; + yield 'query string with slash is added behind base without path' => [ + 'http://example.com/base', + '/?key=value', + 'http://example.com/?key=value', + ]; + yield 'absolute with query string below base is returned as-is' => [ + 'http://example.com/base', + 'http://example.com/base?test', + 'http://example.com/base?test', + ]; + yield 'urlencoded special chars will stay as-is' => [ + 'http://example.com/%7Bversion%7D/', + '', + 'http://example.com/%7Bversion%7D/' + ]; + yield 'special chars will be urlencoded' => [ + 'http://example.com/%7Bversion%7D/', + '', + 'http://example.com/%7Bversion%7D/' + ]; + yield 'other domain' => [ + 'http://example.com/base/', + 'http://example.org/base/', + 'http://example.org/base/' + ]; + yield 'other scheme' => [ + 'http://example.com/base/', + 'https://example.com/base/', + 'https://example.com/base/' + ]; + yield 'other port' => [ + 'http://example.com/base/', + 'http://example.com:81/base/', + 'http://example.com:81/base/' + ]; + yield 'other path' => [ + 'http://example.com/base/', + 'http://example.com/other/', + 'http://example.com/other/' + ]; + yield 'other path due to missing slash' => [ + 'http://example.com/base/', + 'http://example.com/other', + 'http://example.com/other' ]; } @@ -374,13 +375,13 @@ public function testResolveUriWithBaseEndsWithoutSlash($base, $uri, $expectedAbs public function testWithBaseUrlNotAbsoluteFails() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $this->browser->withBase('hello'); } public function testWithBaseUrlInvalidSchemeFails() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $this->browser->withBase('ftp://example.com'); } @@ -410,7 +411,7 @@ public function testWithProtocolVersionFollowedByGetRequestSendsRequestWithProto public function testWithProtocolVersionInvalidThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $this->browser->withProtocolVersion('1.2'); } @@ -418,7 +419,7 @@ public function testCancelGetRequestShouldCancelUnderlyingSocketConnection() { $pending = new Promise(function () { }, $this->expectCallableOnce()); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->with('example.com:80')->willReturn($pending); $this->browser = new Browser($connector, $this->loop); diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index 92c873d1..d89d92e9 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -176,7 +176,8 @@ public function testGetRequestWithRelativeAddressRejects() { $promise = $this->browser->get('delay'); - $this->setExpectedException('InvalidArgumentException', 'Invalid request URL given'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid request URL given'); await($promise); } @@ -201,7 +202,7 @@ public function testCancelGetRequestWillRejectRequest() $promise = $this->browser->get($this->base . 'get'); $promise->cancel(); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); await($promise); } @@ -212,13 +213,13 @@ public function testCancelRequestWithPromiseFollowerWillRejectRequest() }); $promise->cancel(); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); await($promise); } public function testRequestWithoutAuthenticationFails() { - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); await($this->browser->get($this->base . 'basic-auth/user/pass')); } @@ -269,7 +270,8 @@ public function testCancelRedirectedRequestShouldReject() $promise->cancel(); }); - $this->setExpectedException('RuntimeException', 'Request cancelled'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Request cancelled'); await($promise); } @@ -277,7 +279,8 @@ public function testTimeoutDelayedResponseShouldReject() { $promise = $this->browser->withTimeout(0.1)->get($this->base . 'delay/10'); - $this->setExpectedException('RuntimeException', 'Request timed out after 0.1 seconds'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Request timed out after 0.1 seconds'); await($promise); } @@ -287,7 +290,8 @@ public function testTimeoutDelayedResponseAfterStreamingRequestShouldReject() $promise = $this->browser->withTimeout(0.1)->post($this->base . 'delay/10', [], $stream); $stream->end(); - $this->setExpectedException('RuntimeException', 'Request timed out after 0.1 seconds'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Request timed out after 0.1 seconds'); await($promise); } @@ -329,7 +333,7 @@ public function testFollowRedirectsZeroRejectsOnRedirect() { $browser = $this->browser->withFollowRedirects(0); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); await($browser->get($this->base . 'redirect-to?url=get')); } @@ -367,11 +371,9 @@ public function testGetRequestWithResponseBufferExceededRejects() { $promise = $this->browser->withResponseBuffer(4)->get($this->base . 'get'); - $this->setExpectedException( - 'OverflowException', - 'Response body size of 5 bytes exceeds maximum of 4 bytes', - defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 - ); + $this->expectException(\OverflowException::class); + $this->expectExceptionMessage('Response body size of 5 bytes exceeds maximum of 4 bytes'); + $this->expectExceptionCode(defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0); await($promise); } @@ -379,11 +381,9 @@ public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() { $promise = $this->browser->withResponseBuffer(4)->get($this->base . 'stream/1'); - $this->setExpectedException( - 'OverflowException', - 'Response body size exceeds maximum of 4 bytes', - defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0 - ); + $this->expectException(\OverflowException::class); + $this->expectExceptionMessage('Response body size exceeds maximum of 4 bytes'); + $this->expectExceptionCode(defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0); await($promise); } @@ -409,7 +409,7 @@ public function testVerifyPeerEnabledForBadSslRejects() $browser = new Browser($connector); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); await($browser->get('https://self-signed.badssl.com/')); } @@ -435,7 +435,7 @@ public function testVerifyPeerDisabledForBadSslResolves() */ public function testInvalidPort() { - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); await($this->browser->get('http://www.google.com:443/')); } @@ -447,7 +447,7 @@ public function testErrorStatusCodeRejectsWithResponseException() } catch (ResponseException $e) { $this->assertEquals(404, $e->getCode()); - $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $e->getResponse()); + $this->assertInstanceOf(ResponseInterface::class, $e->getResponse()); $this->assertEquals(404, $e->getResponse()->getStatusCode()); } } @@ -762,7 +762,7 @@ public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndKnown $body = $response->getBody(); $this->assertEquals(5, $body->getSize()); $this->assertEquals('', (string) $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); } public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndUnknownSizeFromStreamingEndpoint() @@ -773,7 +773,7 @@ public function testRequestStreamingGetReceivesResponseWithStreamingBodyAndUnkno $body = $response->getBody(); $this->assertNull($body->getSize()); $this->assertEquals('', (string) $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); } public function testRequestStreamingGetReceivesStreamingResponseBody() diff --git a/tests/FunctionalHttpServerTest.php b/tests/FunctionalHttpServerTest.php index c0fcfe83..dc0bd276 100644 --- a/tests/FunctionalHttpServerTest.php +++ b/tests/FunctionalHttpServerTest.php @@ -43,8 +43,8 @@ public function testPlainHttpOnRandomPort() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -70,7 +70,7 @@ function () { $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 404 Not Found", $response); + $this->assertStringContainsString("HTTP/1.0 404 Not Found", $response); $socket->close(); } @@ -94,8 +94,8 @@ public function testPlainHttpOnRandomPortWithoutHostHeaderUsesSocketUri() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('http://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -119,8 +119,8 @@ public function testPlainHttpOnRandomPortWithOtherHostHeaderTakesPrecedence() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('http://localhost:1000/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('http://localhost:1000/', $response); $socket->close(); } @@ -150,8 +150,8 @@ public function testSecureHttpsOnRandomPort() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -181,8 +181,8 @@ public function testSecureHttpsReturnsData() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString("\r\nContent-Length: 33000\r\n", $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString("\r\nContent-Length: 33000\r\n", $response); $this->assertStringEndsWith("\r\n". str_repeat('.', 33000), $response); $socket->close(); @@ -211,8 +211,8 @@ public function testSecureHttpsOnRandomPortWithoutHostHeaderUsesSocketUri() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('https://' . noScheme($socket->getAddress()) . '/', $response); $socket->close(); } @@ -240,8 +240,8 @@ public function testPlainHttpOnStandardPortReturnsUriWithNoPort() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('http://127.0.0.1/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('http://127.0.0.1/', $response); $socket->close(); } @@ -269,8 +269,8 @@ public function testPlainHttpOnStandardPortWithoutHostHeaderReturnsUriWithNoPort $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('http://127.0.0.1/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('http://127.0.0.1/', $response); $socket->close(); } @@ -303,8 +303,8 @@ public function testSecureHttpsOnStandardPortReturnsUriWithNoPort() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('https://127.0.0.1/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('https://127.0.0.1/', $response); $socket->close(); } @@ -337,8 +337,8 @@ public function testSecureHttpsOnStandardPortWithoutHostHeaderUsesSocketUri() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('https://127.0.0.1/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('https://127.0.0.1/', $response); $socket->close(); } @@ -366,8 +366,8 @@ public function testPlainHttpOnHttpsStandardPortReturnsUriWithPort() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('http://127.0.0.1:443/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('http://127.0.0.1:443/', $response); $socket->close(); } @@ -400,8 +400,8 @@ public function testSecureHttpsOnHttpStandardPortReturnsUriWithPort() $response = await(timeout($result, 1.0)); - $this->assertContainsString("HTTP/1.0 200 OK", $response); - $this->assertContainsString('https://127.0.0.1:80/', $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response); + $this->assertStringContainsString('https://127.0.0.1:80/', $response); $socket->close(); } @@ -743,7 +743,7 @@ function (ServerRequestInterface $request) { $responses = await(timeout(all($result), 1.0)); foreach ($responses as $response) { - $this->assertContainsString("HTTP/1.0 200 OK", $response, $response); + $this->assertStringContainsString("HTTP/1.0 200 OK", $response, $response); $this->assertTrue(substr($response, -4) == 1024, $response); } diff --git a/tests/HttpServerTest.php b/tests/HttpServerTest.php index fc977d9b..a62e5fbd 100644 --- a/tests/HttpServerTest.php +++ b/tests/HttpServerTest.php @@ -4,10 +4,14 @@ use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; +use React\EventLoop\LoopInterface; use React\Http\HttpServer; use React\Http\Io\IniUtil; +use React\Http\Middleware\LimitConcurrentRequestsMiddleware; +use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\Middleware\StreamingRequestMiddleware; use React\Promise\Deferred; +use React\Socket\Connection; use React\Stream\ReadableStreamInterface; use function React\Async\await; use function React\Promise\reject; @@ -25,7 +29,7 @@ final class HttpServerTest extends TestCase */ public function setUpConnectionMockAndSocket() { - $this->connection = $this->getMockBuilder('React\Socket\Connection') + $this->connection = $this->getMockBuilder(Connection::class) ->disableOriginalConstructor() ->setMethods( [ @@ -65,12 +69,12 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() $ref->setAccessible(true); $loop = $ref->getValue($clock); - $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + $this->assertInstanceOf(LoopInterface::class, $loop); } public function testInvalidCallbackFunctionLeadsToException() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new HttpServer('invalid'); } @@ -271,8 +275,8 @@ public function testForwardErrors() $data = $this->createPostFileUploadRequest(); $this->connection->emit('data', [implode('', $data)]); - $this->assertInstanceOf('RuntimeException', $capturedException); - $this->assertInstanceOf('Exception', $capturedException->getPrevious()); + $this->assertInstanceOf(\RuntimeException::class, $capturedException); + $this->assertInstanceOf(\Exception::class, $capturedException->getPrevious()); $this->assertSame($exception, $capturedException->getPrevious()); } @@ -297,24 +301,22 @@ private function createPostFileUploadRequest() return $data; } - public function provideIniSettingsForConcurrency() + public static function provideIniSettingsForConcurrency() { - return [ - 'default settings' => [ - '128M', - '64K', // 8M capped at maximum - 1024 - ], - 'unlimited memory_limit has no concurrency limit' => [ - '-1', - '8M', - null - ], - 'small post_max_size results in high concurrency' => [ - '128M', - '1k', - 65536 - ] + yield 'default settings' => [ + '128M', + '64K', // 8M capped at maximum + 1024 + ]; + yield 'unlimited memory_limit has no concurrency limit' => [ + '-1', + '8M', + null + ]; + yield 'small post_max_size results in high concurrency' => [ + '128M', + '1k', + 65536 ]; } @@ -401,7 +403,7 @@ public function testConstructServerWithUnlimitedMemoryLimitDoesNotLimitConcurren $middleware = $ref->getValue($middlewareRunner); $this->assertTrue(is_array($middleware)); - $this->assertInstanceOf('React\Http\Middleware\RequestBodyBufferMiddleware', $middleware[0]); + $this->assertInstanceOf(RequestBodyBufferMiddleware::class, $middleware[0]); } public function testConstructServerWithMemoryLimitDoesLimitConcurrency() @@ -431,7 +433,7 @@ public function testConstructServerWithMemoryLimitDoesLimitConcurrency() $middleware = $ref->getValue($middlewareRunner); $this->assertTrue(is_array($middleware)); - $this->assertInstanceOf('React\Http\Middleware\LimitConcurrentRequestsMiddleware', $middleware[0]); + $this->assertInstanceOf(LimitConcurrentRequestsMiddleware::class, $middleware[0]); } public function testConstructFiltersOutConfigurationMiddlewareBefore() diff --git a/tests/Io/AbstractMessageTest.php b/tests/Io/AbstractMessageTest.php index 59c170ec..5451281a 100644 --- a/tests/Io/AbstractMessageTest.php +++ b/tests/Io/AbstractMessageTest.php @@ -26,7 +26,7 @@ public function testWithProtocolVersionReturnsNewInstanceWhenProtocolVersionIsCh $message = new MessageMock( '1.1', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() + $this->createMock(StreamInterface::class) ); $new = $message->withProtocolVersion('1.0'); @@ -40,7 +40,7 @@ public function testWithProtocolVersionReturnsSameInstanceWhenProtocolVersionIsU $message = new MessageMock( '1.1', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() + $this->createMock(StreamInterface::class) ); $new = $message->withProtocolVersion('1.1'); @@ -55,7 +55,7 @@ public function testHeaderWithStringValue() [ 'Content-Type' => 'text/plain' ], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() + $this->createMock(StreamInterface::class) ); $this->assertEquals(['Content-Type' => ['text/plain']], $message->getHeaders()); @@ -109,7 +109,7 @@ public function testHeaderWithMultipleValues() 'b=2' ] ], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() + $this->createMock(StreamInterface::class) ); $this->assertEquals(['Set-Cookie' => ['a=1', 'b=2']], $message->getHeaders()); @@ -152,7 +152,7 @@ public function testHeaderWithEmptyValue() [ 'Content-Type' => [] ], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() + $this->createMock(StreamInterface::class) ); $this->assertEquals([], $message->getHeaders()); @@ -183,7 +183,7 @@ public function testHeaderWithMultipleValuesAcrossMixedCaseNamesInConstructorMer 'set-cookie' => ['b=2'], 'set-COOKIE' => [] ], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock() + $this->createMock(StreamInterface::class) ); $this->assertEquals(['set-cookie' => ['a=1', 'b=2']], $message->getHeaders()); @@ -192,14 +192,14 @@ public function testHeaderWithMultipleValuesAcrossMixedCaseNamesInConstructorMer public function testWithBodyReturnsNewInstanceWhenBodyIsChanged() { - $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); + $body = $this->createMock(StreamInterface::class); $message = new MessageMock( '1.1', [], $body ); - $body2 = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); + $body2 = $this->createMock(StreamInterface::class); $new = $message->withBody($body2); $this->assertNotSame($message, $new); $this->assertSame($body2, $new->getBody()); @@ -208,7 +208,7 @@ public function testWithBodyReturnsNewInstanceWhenBodyIsChanged() public function testWithBodyReturnsSameInstanceWhenBodyIsUnchanged() { - $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); + $body = $this->createMock(StreamInterface::class); $message = new MessageMock( '1.1', [], diff --git a/tests/Io/AbstractRequestTest.php b/tests/Io/AbstractRequestTest.php index 24990622..5d41369b 100644 --- a/tests/Io/AbstractRequestTest.php +++ b/tests/Io/AbstractRequestTest.php @@ -32,12 +32,12 @@ class AbstractRequestTest extends TestCase { public function testCtorWithInvalidUriThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new RequestMock( 'GET', null, [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); } @@ -48,7 +48,7 @@ public function testGetHeadersReturnsHostHeaderFromUri() 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -61,7 +61,7 @@ public function testGetHeadersReturnsHostHeaderFromUriWithCustomHttpPort() 'GET', 'http://example.com:8080/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -74,7 +74,7 @@ public function testGetHeadersReturnsHostHeaderFromUriWithCustomPortHttpOnHttpsP 'GET', 'http://example.com:443/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -87,7 +87,7 @@ public function testGetHeadersReturnsHostHeaderFromUriWithCustomPortHttpsOnHttpP 'GET', 'https://example.com:80/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -100,7 +100,7 @@ public function testGetHeadersReturnsHostHeaderFromUriWithoutDefaultHttpPort() 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -113,7 +113,7 @@ public function testGetHeadersReturnsHostHeaderFromUriWithoutDefaultHttpsPort() 'GET', 'https://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -128,7 +128,7 @@ public function testGetHeadersReturnsHostHeaderFromUriBeforeOtherHeadersExplicit [ 'User-Agent' => 'demo' ], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -143,7 +143,7 @@ public function testGetHeadersReturnsHostHeaderFromHeadersExplicitlyGiven() [ 'Host' => 'example.com:8080' ], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -158,7 +158,7 @@ public function testGetHeadersReturnsHostHeaderFromUriWhenHeadersExplicitlyGiven [ 'Host' => [] ], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -171,7 +171,7 @@ public function testGetRequestTargetReturnsPathAndQueryFromUri() 'GET', 'http://example.com/demo?name=Alice', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -184,7 +184,7 @@ public function testGetRequestTargetReturnsSlashOnlyIfUriHasNoPathOrQuery() 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -197,7 +197,7 @@ public function testGetRequestTargetReturnsRequestTargetInAbsoluteFormIfGivenExp 'GET', 'http://example.com/demo?name=Alice', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); $request = $request->withRequestTarget('http://example.com/demo?name=Alice'); @@ -211,7 +211,7 @@ public function testWithRequestTargetReturnsNewInstanceWhenRequestTargetIsChange 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -227,7 +227,7 @@ public function testWithRequestTargetReturnsSameInstanceWhenRequestTargetIsUncha 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); $request = $request->withRequestTarget('/'); @@ -243,7 +243,7 @@ public function testWithMethodReturnsNewInstanceWhenMethodIsChanged() 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -259,7 +259,7 @@ public function testWithMethodReturnsSameInstanceWhenMethodIsUnchanged() 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -270,13 +270,13 @@ public function testWithMethodReturnsSameInstanceWhenMethodIsUnchanged() public function testGetUriReturnsUriInstanceGivenToCtor() { - $uri = $this->getMockBuilder('Psr\Http\Message\UriInterface')->getMock(); + $uri = $this->createMock(UriInterface::class); $request = new RequestMock( 'GET', $uri, [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -289,12 +289,12 @@ public function testGetUriReturnsUriInstanceForUriStringGivenToCtor() 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); $uri = $request->getUri(); - $this->assertInstanceOf('Psr\Http\Message\UriInterface', $uri); + $this->assertInstanceOf(UriInterface::class, $uri); $this->assertEquals('http://example.com/', (string) $uri); } @@ -304,11 +304,11 @@ public function testWithUriReturnsNewInstanceWhenUriIsChanged() 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); - $uri = $this->getMockBuilder('Psr\Http\Message\UriInterface')->getMock(); + $uri = $this->createMock(UriInterface::class); $new = $request->withUri($uri); $this->assertNotSame($request, $new); @@ -318,13 +318,13 @@ public function testWithUriReturnsNewInstanceWhenUriIsChanged() public function testWithUriReturnsSameInstanceWhenUriIsUnchanged() { - $uri = $this->getMockBuilder('Psr\Http\Message\UriInterface')->getMock(); + $uri = $this->createMock(UriInterface::class); $request = new RequestMock( 'GET', $uri, [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -339,7 +339,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderChangedIfUriContainsH 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -357,7 +357,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderChangedIfUriContainsH 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -377,7 +377,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderBef [ 'User-Agent' => 'test' ], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); $request = $request->withoutHeader('Host'); @@ -396,7 +396,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfUriContain 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -414,7 +414,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderUnchangedIfPreserveHo 'GET', 'http://example.com/', [], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); @@ -434,7 +434,7 @@ public function testWithUriReturnsNewInstanceWithHostHeaderAddedAsFirstHeaderNoM [ 'User-Agent' => 'test' ], - $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(), + $this->createMock(StreamInterface::class), '1.1' ); $request = $request->withoutHeader('Host'); diff --git a/tests/Io/BufferedBodyTest.php b/tests/Io/BufferedBodyTest.php index 4f5d042a..c8534d50 100644 --- a/tests/Io/BufferedBodyTest.php +++ b/tests/Io/BufferedBodyTest.php @@ -91,7 +91,7 @@ public function testSeekBeforeStartThrows() } catch (\RuntimeException $e) { $this->assertSame(0, $stream->tell()); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); throw $e; } } @@ -100,7 +100,7 @@ public function testSeekWithInvalidModeThrows() { $stream = new BufferedBody('hello'); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $stream->seek(1, 12345); } @@ -109,7 +109,7 @@ public function testSeekAfterCloseThrows() $stream = new BufferedBody('hello'); $stream->close(); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); $stream->seek(0); } @@ -118,7 +118,7 @@ public function testTellAfterCloseThrows() $stream = new BufferedBody('hello'); $stream->close(); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); $stream->tell(); } @@ -136,7 +136,7 @@ public function testRewindAfterCloseThrows() $stream = new BufferedBody('hello'); $stream->close(); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); $stream->rewind(); } @@ -180,7 +180,7 @@ public function testReadZeroThrows() { $stream = new BufferedBody('hello'); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $stream->read(0); } @@ -189,7 +189,7 @@ public function testReadAfterCloseThrows() $stream = new BufferedBody('hello'); $stream->close(); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); $stream->read(10); } @@ -218,7 +218,7 @@ public function testGetContentsAfterCloseThrows() $stream = new BufferedBody('hello'); $stream->close(); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); $stream->getContents(); } @@ -280,7 +280,7 @@ public function testWriteAfterCloseThrows() $stream = new BufferedBody('hello'); $stream->close(); - $this->setExpectedException('RuntimeException'); + $this->expectException(\RuntimeException::class); $stream->write('foo'); } diff --git a/tests/Io/ChunkedDecoderTest.php b/tests/Io/ChunkedDecoderTest.php index 5168f2d0..3ae8c742 100644 --- a/tests/Io/ChunkedDecoderTest.php +++ b/tests/Io/ChunkedDecoderTest.php @@ -3,7 +3,9 @@ namespace React\Tests\Http\Io; use React\Http\Io\ChunkedDecoder; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; +use React\Stream\WritableStreamInterface; use React\Tests\Http\TestCase; class ChunkedDecoderTest extends TestCase @@ -394,7 +396,7 @@ public function testHandleError() public function testPauseStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $parser = new ChunkedDecoder($input); @@ -403,7 +405,7 @@ public function testPauseStream() public function testResumeStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $parser = new ChunkedDecoder($input); @@ -413,7 +415,7 @@ public function testResumeStream() public function testPipeStream() { - $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $dest = $this->createMock(WritableStreamInterface::class); $ret = $this->parser->pipe($dest); diff --git a/tests/Io/ChunkedEncoderTest.php b/tests/Io/ChunkedEncoderTest.php index 96b97848..cbb3e7ad 100644 --- a/tests/Io/ChunkedEncoderTest.php +++ b/tests/Io/ChunkedEncoderTest.php @@ -3,7 +3,9 @@ namespace React\Tests\Http\Io; use React\Http\Io\ChunkedEncoder; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; +use React\Stream\WritableStreamInterface; use React\Tests\Http\TestCase; class ChunkedEncoderTest extends TestCase @@ -59,7 +61,7 @@ public function testHandleError() public function testPauseStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $parser = new ChunkedEncoder($input); @@ -68,7 +70,7 @@ public function testPauseStream() public function testResumeStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $parser = new ChunkedEncoder($input); @@ -78,7 +80,7 @@ public function testResumeStream() public function testPipeStream() { - $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $dest = $this->createMock(WritableStreamInterface::class); $ret = $this->chunkedStream->pipe($dest); diff --git a/tests/Io/ClientConnectionManagerTest.php b/tests/Io/ClientConnectionManagerTest.php index 88c5ff4f..c4b3a07e 100644 --- a/tests/Io/ClientConnectionManagerTest.php +++ b/tests/Io/ClientConnectionManagerTest.php @@ -2,10 +2,14 @@ namespace React\Tests\Http\Io; +use React\EventLoop\LoopInterface; +use React\EventLoop\TimerInterface; use React\Http\Io\ClientConnectionManager; use React\Http\Message\Uri; use React\Promise\Promise; use React\Promise\PromiseInterface; +use React\Socket\ConnectionInterface; +use React\Socket\ConnectorInterface; use React\Tests\Http\TestCase; use function React\Promise\resolve; @@ -14,10 +18,10 @@ class ClientConnectionManagerTest extends TestCase public function testConnectWithHttpsUriShouldConnectToTlsWithDefaultPort() { $promise = new Promise(function () { }); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn($promise); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $connectionManager = new ClientConnectionManager($connector, $loop); @@ -30,10 +34,10 @@ public function testConnectWithHttpsUriShouldConnectToTlsWithDefaultPort() public function testConnectWithHttpUriShouldConnectToTcpWithDefaultPort() { $promise = new Promise(function () { }); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->with('reactphp.org:80')->willReturn($promise); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $connectionManager = new ClientConnectionManager($connector, $loop); @@ -44,10 +48,10 @@ public function testConnectWithHttpUriShouldConnectToTcpWithDefaultPort() public function testConnectWithExplicitPortShouldConnectWithGivenPort() { $promise = new Promise(function () { }); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->with('reactphp.org:8080')->willReturn($promise); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $connectionManager = new ClientConnectionManager($connector, $loop); @@ -57,10 +61,10 @@ public function testConnectWithExplicitPortShouldConnectWithGivenPort() public function testConnectWithInvalidSchemeShouldRejectWithException() { - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->never())->method('connect'); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $connectionManager = new ClientConnectionManager($connector, $loop); @@ -71,16 +75,16 @@ public function testConnectWithInvalidSchemeShouldRejectWithException() $exception = $reason; }); - $this->assertInstanceOf('InvalidArgumentException', $exception); + $this->assertInstanceOf(\InvalidArgumentException::class, $exception); $this->assertEquals('Invalid request URL given', $exception->getMessage()); } public function testConnectWithoutSchemeShouldRejectWithException() { - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->never())->method('connect'); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $connectionManager = new ClientConnectionManager($connector, $loop); @@ -91,13 +95,13 @@ public function testConnectWithoutSchemeShouldRejectWithException() $exception = $reason; }); - $this->assertInstanceOf('InvalidArgumentException', $exception); + $this->assertInstanceOf(\InvalidArgumentException::class, $exception); $this->assertEquals('Invalid request URL given', $exception->getMessage()); } public function testConnectReusesIdleConnectionFromPreviousKeepAliveCallWithoutUsingConnectorAndWillAddAndRemoveStreamEventsAndAddAndCancelIdleTimer() { - $connectionToReuse = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connectionToReuse = $this->createMock(ConnectionInterface::class); $streamHandler = null; $connectionToReuse->expects($this->exactly(3))->method('on')->withConsecutive( @@ -148,11 +152,11 @@ public function testConnectReusesIdleConnectionFromPreviousKeepAliveCallWithoutU ] ); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->never())->method('connect'); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); @@ -173,13 +177,13 @@ public function testConnectReusesIdleConnectionFromPreviousKeepAliveCallWithoutU public function testConnectReusesIdleConnectionFromPreviousKeepAliveCallWithoutUsingConnectorAlsoWhenUriPathAndQueryAndFragmentIsDifferent() { - $connectionToReuse = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connectionToReuse = $this->createMock(ConnectionInterface::class); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->never())->method('connect'); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); @@ -200,13 +204,13 @@ public function testConnectReusesIdleConnectionFromPreviousKeepAliveCallWithoutU public function testConnectUsesConnectorWithSameUriAndReturnsPromiseForNewConnectionFromConnectorWhenPreviousKeepAliveCallUsedDifferentUri() { - $connectionToReuse = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connectionToReuse = $this->createMock(ConnectionInterface::class); $promise = new Promise(function () { }); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn($promise); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $connectionManager = new ClientConnectionManager($connector, $loop); @@ -220,14 +224,14 @@ public function testConnectUsesConnectorWithSameUriAndReturnsPromiseForNewConnec public function testConnectUsesConnectorForNewConnectionWhenPreviousConnectReusedIdleConnectionFromPreviousKeepAliveCall() { - $firstConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); - $secondConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $firstConnection = $this->createMock(ConnectionInterface::class); + $secondConnection = $this->createMock(ConnectionInterface::class); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(resolve($secondConnection)); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); @@ -251,12 +255,12 @@ public function testConnectUsesConnectorForNewConnectionWhenPreviousConnectReuse public function testKeepAliveAddsTimerAndDoesNotCloseConnectionImmediately() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->never())->method('close'); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything()); $connectionManager = new ClientConnectionManager($connector, $loop); @@ -266,14 +270,14 @@ public function testKeepAliveAddsTimerAndDoesNotCloseConnectionImmediately() public function testKeepAliveClosesConnectionAfterIdleTimeout() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('close'); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $timerCallback = null; - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timerCallback) { $timerCallback = $cb; return true; @@ -291,18 +295,18 @@ public function testKeepAliveClosesConnectionAfterIdleTimeout() public function testConnectUsesConnectorForNewConnectionWhenIdleConnectionFromPreviousKeepAliveCallHasAlreadyTimedOut() { - $firstConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $firstConnection = $this->createMock(ConnectionInterface::class); $firstConnection->expects($this->once())->method('close'); - $secondConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $secondConnection = $this->createMock(ConnectionInterface::class); $secondConnection->expects($this->never())->method('close'); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(resolve($secondConnection)); $timerCallback = null; - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timerCallback) { $timerCallback = $cb; return true; @@ -330,7 +334,7 @@ public function testConnectUsesConnectorForNewConnectionWhenIdleConnectionFromPr public function testConnectUsesConnectorForNewConnectionWhenIdleConnectionFromPreviousKeepAliveCallHasAlreadyFiredUnexpectedStreamEventBeforeIdleTimeoutThatClosesConnection() { - $firstConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $firstConnection = $this->createMock(ConnectionInterface::class); $firstConnection->expects($this->once())->method('close'); $streamHandler = null; @@ -358,14 +362,14 @@ public function testConnectUsesConnectorForNewConnectionWhenIdleConnectionFromPr ] ); - $secondConnection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $secondConnection = $this->createMock(ConnectionInterface::class); $secondConnection->expects($this->never())->method('close'); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->with('tls://reactphp.org:443')->willReturn(resolve($secondConnection)); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); diff --git a/tests/Io/ClientRequestStreamTest.php b/tests/Io/ClientRequestStreamTest.php index 0df92961..a20cda61 100644 --- a/tests/Io/ClientRequestStreamTest.php +++ b/tests/Io/ClientRequestStreamTest.php @@ -4,11 +4,15 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use React\EventLoop\LoopInterface; +use React\Http\Io\ClientConnectionManager; use React\Http\Io\ClientRequestStream; use React\Http\Message\Request; use React\Http\Message\Uri; use React\Promise\Deferred; use React\Promise\Promise; +use React\Socket\Connection; +use React\Socket\ConnectionInterface; use React\Stream\DuplexResourceStream; use React\Stream\ReadableStreamInterface; use React\Tests\Http\TestCase; @@ -20,10 +24,10 @@ class ClientRequestStreamTest extends TestCase /** @test */ public function testRequestShouldUseConnectionManagerWithUriFromRequestAndBindToStreamEvents() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $uri = new Uri('http://www.example.com/'); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->with($uri)->willReturn(resolve($connection)); $requestData = new Request('GET', $uri); @@ -55,13 +59,13 @@ public function testRequestShouldUseConnectionManagerWithUriFromRequestAndBindTo /** @test */ public function requestShouldEmitErrorIfConnectionFails() { - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(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('error', $this->expectCallableOnceWith($this->isInstanceOf(\RuntimeException::class))); $request->on('close', $this->expectCallableOnce()); $request->end(); @@ -70,15 +74,15 @@ public function requestShouldEmitErrorIfConnectionFails() /** @test */ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(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('error', $this->expectCallableOnceWith($this->isInstanceOf(\RuntimeException::class))); $request->on('close', $this->expectCallableOnce()); $request->end(); @@ -88,15 +92,15 @@ public function requestShouldEmitErrorIfConnectionClosesBeforeResponseIsParsed() /** @test */ public function requestShouldEmitErrorIfConnectionEmitsError() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(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('error', $this->expectCallableOnceWith($this->isInstanceOf(\Exception::class))); $request->on('close', $this->expectCallableOnce()); $request->end(); @@ -107,43 +111,41 @@ public static function provideInvalidRequest() { $request = new Request('GET' , "http://localhost/"); - return [ - [ - $request->withMethod("INVA\r\nLID", '') - ], - [ - $request->withRequestTarget('/inva lid') - ], - [ - $request->withHeader('Invalid', "Yes\r\n") - ], - [ - $request->withHeader('Invalid', "Yes\n") - ], - [ - $request->withHeader('Invalid', "Yes\r") - ], - [ - $request->withHeader("Inva\r\nlid", 'Yes') - ], - [ - $request->withHeader("Inva\nlid", 'Yes') - ], - [ - $request->withHeader("Inva\rlid", 'Yes') - ], - [ - $request->withHeader('Inva Lid', 'Yes') - ], - [ - $request->withHeader('Inva:Lid', 'Yes') - ], - [ - $request->withHeader('Invalid', "Val\0ue") - ], - [ - $request->withHeader("Inva\0lid", 'Yes') - ] + yield [ + $request->withMethod("INVA\r\nLID", '') + ]; + yield [ + $request->withRequestTarget('/inva lid') + ]; + yield [ + $request->withHeader('Invalid', "Yes\r\n") + ]; + yield [ + $request->withHeader('Invalid', "Yes\n") + ]; + yield [ + $request->withHeader('Invalid', "Yes\r") + ]; + yield [ + $request->withHeader("Inva\r\nlid", 'Yes') + ]; + yield [ + $request->withHeader("Inva\nlid", 'Yes') + ]; + yield [ + $request->withHeader("Inva\rlid", 'Yes') + ]; + yield [ + $request->withHeader('Inva Lid', 'Yes') + ]; + yield [ + $request->withHeader('Inva:Lid', 'Yes') + ]; + yield [ + $request->withHeader('Invalid', "Val\0ue") + ]; + yield [ + $request->withHeader("Inva\0lid", 'Yes') ]; } @@ -153,12 +155,12 @@ public static function provideInvalidRequest() */ public function testStreamShouldEmitErrorBeforeCreatingConnectionWhenRequestIsInvalid(RequestInterface $request) { - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->never())->method('connect'); $stream = new ClientRequestStream($connectionManager, $request); - $stream->on('error', $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); + $stream->on('error', $this->expectCallableOnceWith($this->isInstanceOf(\InvalidArgumentException::class))); $stream->on('close', $this->expectCallableOnce()); $stream->end(); @@ -167,15 +169,15 @@ public function testStreamShouldEmitErrorBeforeCreatingConnectionWhenRequestIsIn /** @test */ public function requestShouldEmitErrorIfRequestParserThrowsException() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(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('error', $this->expectCallableOnceWith($this->isInstanceOf(\InvalidArgumentException::class))); $request->on('close', $this->expectCallableOnce()); $request->end(); @@ -185,10 +187,10 @@ public function requestShouldEmitErrorIfRequestParserThrowsException() /** @test */ public function getRequestShouldSendAGetRequest() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/', [], '', '1.0'); @@ -200,10 +202,10 @@ public function getRequestShouldSendAGetRequest() /** @test */ public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionCloseHeader() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/', ['Connection' => 'close'], '', '1.1'); @@ -215,10 +217,10 @@ public function getHttp11RequestShouldSendAGetRequestWithGivenConnectionCloseHea /** @test */ public function getOptionsAsteriskShouldSendAOptionsRequestAsteriskRequestTarget() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('OPTIONS', 'http://www.example.com/', ['Connection' => 'close'], '', '1.1'); @@ -230,11 +232,11 @@ public function getOptionsAsteriskShouldSendAOptionsRequestAsteriskRequestTarget public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsContentLengthZero() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/', ['Connection' => 'close'], '', '1.1'); @@ -254,11 +256,11 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsCon public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsStatusNoContent() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/', ['Connection' => 'close'], '', '1.1'); @@ -278,11 +280,11 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsSta public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsStatusNotModifiedWithContentLengthGiven() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/', ['Connection' => 'close'], '', '1.1'); @@ -302,11 +304,11 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenResponseContainsSta public function testStreamShouldEmitResponseWithEmptyBodyWhenRequestMethodIsHead() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('HEAD', 'http://www.example.com/', ['Connection' => 'close'], '', '1.1'); @@ -326,11 +328,11 @@ public function testStreamShouldEmitResponseWithEmptyBodyWhenRequestMethodIsHead public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenResponseContainsContentLengthAndResponseBody() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/', ['Connection' => 'close'], '', '1.1'); @@ -350,11 +352,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResponseContainsContentLengthWithoutResponseBody() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/', ['Connection' => 'close'], '', '1.1'); @@ -374,11 +376,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResp public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndWhenResponseContainsContentLengthWithIncompleteResponseBody() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/', ['Connection' => 'close'], '', '1.1'); @@ -398,11 +400,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndW public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenResponseContainsTransferEncodingChunkedAndResponseBody() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/', ['Connection' => 'close'], '', '1.1'); @@ -422,11 +424,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResponseContainsTransferEncodingChunkedWithoutResponseBody() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/', ['Connection' => 'close'], '', '1.1'); @@ -446,11 +448,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithoutDataWhenResp public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndWhenResponseContainsTransferEncodingChunkedWithIncompleteResponseBody() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/', ['Connection' => 'close'], '', '1.1'); @@ -470,11 +472,11 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndW public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndWhenResponseContainsNoContentLengthAndIncompleteResponseBody() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/', ['Connection' => 'close'], '', '1.1'); @@ -494,7 +496,7 @@ public function testStreamShouldEmitResponseWithStreamingBodyWithDataWithoutEndW public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenResponseContainsNoContentLengthAndResponseBodyTerminatedByConnectionEndEvent() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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'); @@ -510,7 +512,7 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons return true; })); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/', ['Connection' => 'close'], '', '1.1'); @@ -533,12 +535,12 @@ public function testStreamShouldEmitResponseWithStreamingBodyUntilEndWhenRespons public function testStreamShouldReuseConnectionForHttp11ByDefault() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com/'), $connection); @@ -554,12 +556,12 @@ public function testStreamShouldReuseConnectionForHttp11ByDefault() public function testStreamShouldNotReuseConnectionWhenResponseContainsConnectionClose() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/', [], '', '1.1'); @@ -574,12 +576,12 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsConnection public function testStreamShouldNotReuseConnectionWhenRequestContainsConnectionCloseWithAdditionalOptions() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/', ['Connection' => 'FOO, CLOSE, BAR'], '', '1.1'); @@ -594,12 +596,12 @@ public function testStreamShouldNotReuseConnectionWhenRequestContainsConnectionC public function testStreamShouldNotReuseConnectionForHttp10ByDefault() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/', [], '', '1.0'); @@ -614,12 +616,12 @@ public function testStreamShouldNotReuseConnectionForHttp10ByDefault() public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndResponseContainConnectionKeepAlive() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com/'), $connection); @@ -635,12 +637,12 @@ public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndRespon public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndResponseContainConnectionKeepAliveWithAdditionalOptions() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com/'), $connection); @@ -656,7 +658,7 @@ public function testStreamShouldReuseConnectionForHttp10WhenBothRequestAndRespon public function testStreamShouldNotReuseConnectionWhenResponseContainsNoContentLengthAndResponseBodyTerminatedByConnectionEndEvent() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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'); @@ -673,7 +675,7 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsNoContentL return true; })); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/', [], '', '1.1'); @@ -691,7 +693,7 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsNoContentL public function testStreamShouldNotReuseConnectionWhenResponseContainsContentLengthButIsTerminatedByUnexpectedCloseEvent() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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'); @@ -708,7 +710,7 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsContentLen return true; })); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/', [], '', '1.1'); @@ -726,12 +728,12 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsContentLen public function testStreamShouldReuseConnectionWhenResponseContainsTransferEncodingChunkedAndResponseBody() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $connectionManager->expects($this->once())->method('keepAlive')->with(new Uri('http://www.example.com/'), $connection); @@ -747,12 +749,12 @@ public function testStreamShouldReuseConnectionWhenResponseContainsTransferEncod public function testStreamShouldNotReuseConnectionWhenResponseContainsTransferEncodingChunkedAndResponseBodyContainsInvalidData() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/', [], '', '1.1'); @@ -768,10 +770,10 @@ public function testStreamShouldNotReuseConnectionWhenResponseContainsTransferEn /** @test */ public function postRequestShouldSendAPostRequest() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $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 = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('POST', 'http://www.example.com/', [], '', '1.0'); @@ -787,14 +789,14 @@ public function postRequestShouldSendAPostRequest() /** @test */ public function writeWithAPostRequestShouldSendToTheStream() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->exactly(3))->method('write')->withConsecutive( [$this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")], [$this->identicalTo("post")], [$this->identicalTo("data")] ); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('POST', 'http://www.example.com/', [], '', '1.0'); @@ -812,7 +814,7 @@ public function writeWithAPostRequestShouldSendToTheStream() /** @test */ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->exactly(2))->method('write')->withConsecutive( [$this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsomepost$#")], [$this->identicalTo("data")] @@ -821,7 +823,7 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent ); $deferred = new Deferred(); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn($deferred->promise()); $requestData = new Request('POST', 'http://www.example.com/', [], '', '1.0'); @@ -846,7 +848,7 @@ public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent /** @test */ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsBuffer() { - $connection = $this->getMockBuilder('React\Socket\Connection') + $connection = $this->getMockBuilder(Connection::class) ->disableOriginalConstructor() ->setMethods(['write']) ->getMock(); @@ -859,7 +861,7 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB ); $deferred = new Deferred(); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn($deferred->promise()); $requestData = new Request('POST', 'http://www.example.com/', [], '', '1.0'); @@ -885,22 +887,20 @@ public function writeWithAPostRequestShouldForwardDrainEventIfFirstChunkExceedsB /** @test */ public function pipeShouldPipeDataIntoTheRequestBody() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->exactly(3))->method('write')->withConsecutive( [$this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\n\r\nsome$#")], [$this->identicalTo("post")], [$this->identicalTo("data")] ); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('POST', 'http://www.example.com/', [], '', '1.0'); $request = new ClientRequestStream($connectionManager, $requestData); - $loop = $this - ->getMockBuilder('React\EventLoop\LoopInterface') - ->getMock(); + $loop = $this->createMock(LoopInterface::class); $stream = fopen('php://memory', 'r+'); $stream = new DuplexResourceStream($stream, $loop); @@ -920,7 +920,7 @@ public function pipeShouldPipeDataIntoTheRequestBody() */ public function writeShouldStartConnecting() { - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(new Promise(function () { })); $requestData = new Request('POST', 'http://www.example.com/'); @@ -934,7 +934,7 @@ public function writeShouldStartConnecting() */ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() { - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(new Promise(function () { })); $requestData = new Request('POST', 'http://www.example.com/'); @@ -950,7 +950,7 @@ public function endShouldStartConnectingAndChangeStreamIntoNonWritableMode() */ public function closeShouldEmitCloseEvent() { - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $requestData = new Request('POST', 'http://www.example.com/'); $request = new ClientRequestStream($connectionManager, $requestData); @@ -964,7 +964,7 @@ public function closeShouldEmitCloseEvent() */ public function writeAfterCloseReturnsFalse() { - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $requestData = new Request('POST', 'http://www.example.com/'); $request = new ClientRequestStream($connectionManager, $requestData); @@ -980,7 +980,7 @@ public function writeAfterCloseReturnsFalse() */ public function endAfterCloseIsNoOp() { - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->never())->method('connect'); $requestData = new Request('POST', 'http://www.example.com/'); @@ -998,7 +998,7 @@ public function closeShouldCancelPendingConnectionAttempt() $promise = new Promise(function () {}, function () { throw new \RuntimeException(); }); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn($promise); $requestData = new Request('POST', 'http://www.example.com/'); @@ -1016,7 +1016,7 @@ public function closeShouldCancelPendingConnectionAttempt() /** @test */ public function requestShouldRemoveAllListenerAfterClosed() { - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $requestData = new Request('GET', 'http://www.example.com/'); $request = new ClientRequestStream($connectionManager, $requestData); @@ -1031,9 +1031,9 @@ public function requestShouldRemoveAllListenerAfterClosed() /** @test */ public function multivalueHeader() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); - $connectionManager = $this->getMockBuilder('React\Http\Io\ClientConnectionManager')->disableOriginalConstructor()->getMock(); + $connectionManager = $this->createMock(ClientConnectionManager::class); $connectionManager->expects($this->once())->method('connect')->willReturn(resolve($connection)); $requestData = new Request('GET', 'http://www.example.com/'); @@ -1054,7 +1054,7 @@ public function multivalueHeader() $request->handleData("\r\nbody"); /** @var \Psr\Http\Message\ResponseInterface $response */ - $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $response); + $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals('1.0', $response->getProtocolVersion()); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('OK', $response->getReasonPhrase()); diff --git a/tests/Io/ClockTest.php b/tests/Io/ClockTest.php index 8f4b90fa..318fa7ef 100644 --- a/tests/Io/ClockTest.php +++ b/tests/Io/ClockTest.php @@ -3,13 +3,14 @@ namespace React\Tests\Http\Io; use PHPUnit\Framework\TestCase; +use React\EventLoop\LoopInterface; use React\Http\Io\Clock; class ClockTest extends TestCase { public function testNowReturnsSameTimestampMultipleTimesInSameTick() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $clock = new Clock($loop); @@ -21,7 +22,7 @@ public function testNowReturnsSameTimestampMultipleTimesInSameTick() public function testNowResetsMemoizedTimestampOnFutureTick() { $tick = null; - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('futureTick')->with($this->callback(function ($cb) use (&$tick) { $tick = $cb; return true; diff --git a/tests/Io/CloseProtectionStreamTest.php b/tests/Io/CloseProtectionStreamTest.php index 4f3d35ca..f3aa346d 100644 --- a/tests/Io/CloseProtectionStreamTest.php +++ b/tests/Io/CloseProtectionStreamTest.php @@ -3,14 +3,16 @@ namespace React\Tests\Http\Io; use React\Http\Io\CloseProtectionStream; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; +use React\Stream\WritableStreamInterface; use React\Tests\Http\TestCase; class CloseProtectionStreamTest extends TestCase { public function testCloseDoesNotCloseTheInputStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->disableOriginalConstructor()->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->never())->method('pause'); $input->expects($this->never())->method('resume'); $input->expects($this->never())->method('close'); @@ -35,7 +37,7 @@ public function testErrorWontCloseStream() public function testResumeStreamWillResumeInputStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $input->expects($this->once())->method('resume'); @@ -46,7 +48,7 @@ public function testResumeStreamWillResumeInputStream() public function testCloseResumesInputStreamIfItWasPreviouslyPaused() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $input->expects($this->once())->method('resume'); @@ -73,7 +75,7 @@ public function testPipeStream() $input = new ThroughStream(); $protection = new CloseProtectionStream($input); - $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $dest = $this->createMock(WritableStreamInterface::class); $ret = $protection->pipe($dest); @@ -132,7 +134,7 @@ public function testEndWontBeEmittedAfterClose() public function testPauseAfterCloseHasNoEffect() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->never())->method('pause'); $input->expects($this->never())->method('resume'); @@ -146,7 +148,7 @@ public function testPauseAfterCloseHasNoEffect() public function testResumeAfterCloseHasNoEffect() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->never())->method('pause'); $input->expects($this->never())->method('resume'); diff --git a/tests/Io/EmptyBodyStreamTest.php b/tests/Io/EmptyBodyStreamTest.php index 3633ff81..4ee92364 100644 --- a/tests/Io/EmptyBodyStreamTest.php +++ b/tests/Io/EmptyBodyStreamTest.php @@ -3,6 +3,7 @@ namespace React\Tests\Http\Io; use React\Http\Io\EmptyBodyStream; +use React\Stream\WritableStreamInterface; use React\Tests\Http\TestCase; class EmptyBodyStreamTest extends TestCase @@ -36,7 +37,7 @@ public function testResumeIsNoop() public function testPipeStreamReturnsDestinationStream() { - $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $dest = $this->createMock(WritableStreamInterface::class); $ret = $this->bodyStream->pipe($dest); @@ -70,13 +71,13 @@ public function testCloseTwiceEmitsCloseEventAndClearsListeners() public function testTell() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->tell(); } public function testEof() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->eof(); } @@ -87,13 +88,13 @@ public function testIsSeekable() public function testWrite() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->write(''); } public function testRead() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->read(1); } @@ -126,13 +127,13 @@ public function testIsReadableReturnsFalseWhenAlreadyClosed() public function testSeek() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->seek(''); } public function testRewind() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->rewind(); } diff --git a/tests/Io/HttpBodyStreamTest.php b/tests/Io/HttpBodyStreamTest.php index 1fd269b1..c246bd96 100644 --- a/tests/Io/HttpBodyStreamTest.php +++ b/tests/Io/HttpBodyStreamTest.php @@ -3,7 +3,9 @@ namespace React\Tests\Http\Io; use React\Http\Io\HttpBodyStream; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; +use React\Stream\WritableStreamInterface; use React\Tests\Http\TestCase; class HttpBodyStreamTest extends TestCase @@ -28,7 +30,7 @@ public function testDataEmit() public function testPauseStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $bodyStream = new HttpBodyStream($input, null); @@ -37,7 +39,7 @@ public function testPauseStream() public function testResumeStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('resume'); $bodyStream = new HttpBodyStream($input, null); @@ -46,7 +48,7 @@ public function testResumeStream() public function testPipeStream() { - $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $dest = $this->createMock(WritableStreamInterface::class); $ret = $this->bodyStream->pipe($dest); @@ -107,13 +109,13 @@ public function testGetSizeCustom() public function testTell() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->tell(); } public function testEof() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->eof(); } @@ -124,13 +126,13 @@ public function testIsSeekable() public function testWrite() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->write(''); } public function testRead() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->read(''); } @@ -151,13 +153,13 @@ public function testIsReadable() public function testSeek() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->seek(''); } public function testRewind() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->bodyStream->rewind(); } diff --git a/tests/Io/IniUtilTest.php b/tests/Io/IniUtilTest.php index 0bc9a249..2e9f99c9 100644 --- a/tests/Io/IniUtilTest.php +++ b/tests/Io/IniUtilTest.php @@ -7,41 +7,39 @@ class IniUtilTest extends TestCase { - public function provideIniSizes() + public static function provideIniSizes() { - return [ - [ - '1', - 1, - ], - [ - '10', - 10, - ], - [ - '1024', - 1024, - ], - [ - '1K', - 1024, - ], - [ - '1.5M', - 1572864, - ], - [ - '64M', - 67108864, - ], - [ - '8G', - 8589934592, - ], - [ - '1T', - 1099511627776, - ], + yield [ + '1', + 1, + ]; + yield [ + '10', + 10, + ]; + yield [ + '1024', + 1024, + ]; + yield [ + '1K', + 1024, + ]; + yield [ + '1.5M', + 1572864, + ]; + yield [ + '64M', + 67108864, + ]; + yield [ + '8G', + 8589934592, + ]; + yield [ + '1T', + 1099511627776, ]; } @@ -58,16 +56,14 @@ public function testIniSizeToBytesWithInvalidSuffixReturnsNumberWithoutSuffix() $this->assertEquals('2', IniUtil::iniSizeToBytes('2x')); } - public function provideInvalidInputIniSizeToBytes() + public static function provideInvalidInputIniSizeToBytes() { - return [ - ['-1G'], - ['0G'], - ['foo'], - ['fooK'], - ['1ooL'], - ['1ooL'], - ]; + yield ['-1G']; + yield ['0G']; + yield ['foo']; + yield ['fooK']; + yield ['1ooL']; + yield ['1ooL']; } /** @@ -75,7 +71,7 @@ public function provideInvalidInputIniSizeToBytes() */ public function testInvalidInputIniSizeToBytes($input) { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); IniUtil::iniSizeToBytes($input); } } diff --git a/tests/Io/LengthLimitedStreamTest.php b/tests/Io/LengthLimitedStreamTest.php index f1761a0b..b841257a 100644 --- a/tests/Io/LengthLimitedStreamTest.php +++ b/tests/Io/LengthLimitedStreamTest.php @@ -3,7 +3,9 @@ namespace React\Tests\Http\Io; use React\Http\Io\LengthLimitedStream; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; +use React\Stream\WritableStreamInterface; use React\Tests\Http\TestCase; class LengthLimitedStreamTest extends TestCase @@ -59,7 +61,7 @@ public function testHandleError() public function testPauseStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $stream = new LengthLimitedStream($input, 0); @@ -68,7 +70,7 @@ public function testPauseStream() public function testResumeStream() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $stream = new LengthLimitedStream($input, 0); @@ -79,7 +81,7 @@ public function testResumeStream() public function testPipeStream() { $stream = new LengthLimitedStream($this->input, 0); - $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $dest = $this->createMock(WritableStreamInterface::class); $ret = $stream->pipe($dest); diff --git a/tests/Io/MiddlewareRunnerTest.php b/tests/Io/MiddlewareRunnerTest.php index e46039bf..a2344d3a 100644 --- a/tests/Io/MiddlewareRunnerTest.php +++ b/tests/Io/MiddlewareRunnerTest.php @@ -23,7 +23,8 @@ public function testEmptyMiddlewareStackThrowsException() $middlewares = []; $middlewareStack = new MiddlewareRunner($middlewares); - $this->setExpectedException('RuntimeException', 'No middleware to run'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('No middleware to run'); $middlewareStack($request); } @@ -74,7 +75,8 @@ function (ServerRequestInterface $request) { $request = new ServerRequest('GET', 'http://example.com/'); - $this->setExpectedException('RuntimeException', 'hello'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('hello'); $middleware($request); } @@ -88,11 +90,12 @@ function (ServerRequestInterface $request) { $request = new ServerRequest('GET', 'http://example.com/'); - $this->setExpectedException('Throwable', 'hello'); + $this->expectException(\Throwable::class); + $this->expectExceptionMessage('hello'); $middleware($request); } - public function provideProcessStackMiddlewares() + public static function provideProcessStackMiddlewares() { $processStackA = new ProcessStack(); $processStackB = new ProcessStack(); @@ -101,41 +104,39 @@ public function provideProcessStackMiddlewares() $responseMiddleware = function () { return new Response(200); }; - return [ + yield [ [ - [ - $processStackA, - $responseMiddleware, - ], - 1, + $processStackA, + $responseMiddleware, ], + 1, + ]; + yield [ [ - [ - $processStackB, - $processStackB, - $responseMiddleware, - ], - 2, + $processStackB, + $processStackB, + $responseMiddleware, ], + 2, + ]; + yield [ [ - [ - $processStackC, - $processStackC, - $processStackC, - $responseMiddleware, - ], - 3, + $processStackC, + $processStackC, + $processStackC, + $responseMiddleware, ], + 3, + ]; + yield [ [ - [ - $processStackD, - $processStackD, - $processStackD, - $processStackD, - $responseMiddleware, - ], - 4, + $processStackD, + $processStackD, + $processStackD, + $processStackD, + $responseMiddleware, ], + 4, ]; } @@ -172,19 +173,17 @@ public function testProcessStack(array $middlewares, $expectedCallCount) } } - public function provideErrorHandler() + public static function provideErrorHandler() { - return [ - [ - function (\Exception $e) { - throw $e; - } - ], - [ - function (\Exception $e) { - return reject($e); - } - ] + yield [ + function (\Exception $e) { + throw $e; + } + ]; + yield [ + function (\Exception $e) { + return reject($e); + } ]; } @@ -193,7 +192,7 @@ function (\Exception $e) { */ public function testNextCanBeRunMoreThanOnceWithoutCorruptingTheMiddlewareStack($errorHandler) { - $exception = new \RuntimeException('exception'); + $exception = new \RuntimeException(\exception::class); $retryCalled = 0; $error = null; $retry = function ($request, $next) use (&$error, &$retryCalled) { @@ -276,129 +275,127 @@ function (ServerRequestInterface $request) use (&$receivedRequests) { ); } - public function provideUncommonMiddlewareArrayFormats() + public static function provideUncommonMiddlewareArrayFormats() { - return [ - [ - function () { - $sequence = ''; - - // Numeric index gap - return [ - 0 => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'A'; - - return $next($request); - }, - 2 => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'B'; - - return $next($request); - }, - 3 => function () use (&$sequence) { - return new Response(200, [], $sequence . 'C'); - }, - ]; - }, - 'ABC', - ], - [ - function () { - $sequence = ''; - - // Reversed numeric indexes - return [ - 2 => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'A'; - - return $next($request); - }, - 1 => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'B'; - - return $next($request); - }, - 0 => function () use (&$sequence) { - return new Response(200, [], $sequence . 'C'); - }, - ]; - }, - 'ABC', - ], - [ - function () { - $sequence = ''; - - // Associative array - return [ - 'middleware1' => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'A'; - - return $next($request); - }, - 'middleware2' => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'B'; - - return $next($request); - }, - 'middleware3' => function () use (&$sequence) { - return new Response(200, [], $sequence . 'C'); - }, - ]; - }, - 'ABC', - ], - [ - function () { - $sequence = ''; - - // Associative array with empty or trimmable string keys - return [ - '' => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'A'; - - return $next($request); - }, - ' ' => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'B'; - - return $next($request); - }, - ' ' => function () use (&$sequence) { - return new Response(200, [], $sequence . 'C'); - }, - ]; - }, - 'ABC', - ], - [ - function () { - $sequence = ''; - - // Mixed array keys - return [ - '' => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'A'; - - return $next($request); - }, - 0 => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'B'; - - return $next($request); - }, - 'foo' => function (ServerRequestInterface $request, $next) use (&$sequence) { - $sequence .= 'C'; - - return $next($request); - }, - 2 => function () use (&$sequence) { - return new Response(200, [], $sequence . 'D'); - }, - ]; - }, - 'ABCD', - ], + yield [ + function () { + $sequence = ''; + + // Numeric index gap + return [ + 0 => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'A'; + + return $next($request); + }, + 2 => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'B'; + + return $next($request); + }, + 3 => function () use (&$sequence) { + return new Response(200, [], $sequence . 'C'); + }, + ]; + }, + 'ABC', + ]; + yield [ + function () { + $sequence = ''; + + // Reversed numeric indexes + return [ + 2 => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'A'; + + return $next($request); + }, + 1 => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'B'; + + return $next($request); + }, + 0 => function () use (&$sequence) { + return new Response(200, [], $sequence . 'C'); + }, + ]; + }, + 'ABC', + ]; + yield [ + function () { + $sequence = ''; + + // Associative array + return [ + 'middleware1' => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'A'; + + return $next($request); + }, + 'middleware2' => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'B'; + + return $next($request); + }, + 'middleware3' => function () use (&$sequence) { + return new Response(200, [], $sequence . 'C'); + }, + ]; + }, + 'ABC', + ]; + yield [ + function () { + $sequence = ''; + + // Associative array with empty or trimmable string keys + return [ + '' => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'A'; + + return $next($request); + }, + ' ' => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'B'; + + return $next($request); + }, + ' ' => function () use (&$sequence) { + return new Response(200, [], $sequence . 'C'); + }, + ]; + }, + 'ABC', + ]; + yield [ + function () { + $sequence = ''; + + // Mixed array keys + return [ + '' => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'A'; + + return $next($request); + }, + 0 => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'B'; + + return $next($request); + }, + 'foo' => function (ServerRequestInterface $request, $next) use (&$sequence) { + $sequence .= 'C'; + + return $next($request); + }, + 2 => function () use (&$sequence) { + return new Response(200, [], $sequence . 'D'); + }, + ]; + }, + 'ABCD', ]; } diff --git a/tests/Io/MultipartParserTest.php b/tests/Io/MultipartParserTest.php index ba439760..ebc5972f 100644 --- a/tests/Io/MultipartParserTest.php +++ b/tests/Io/MultipartParserTest.php @@ -2,6 +2,7 @@ namespace React\Tests\Http\Io\Middleware; +use Psr\Http\Message\UploadedFileInterface; use React\Http\Io\MultipartParser; use React\Http\Message\ServerRequest; use React\Tests\Http\TestCase; @@ -673,7 +674,7 @@ public function testInvalidUploadFileWithoutContentTypeUsesNullValue() $this->assertCount(1, $files); $this->assertTrue(isset($files['file'])); - $this->assertInstanceOf('Psr\Http\Message\UploadedFileInterface', $files['file']); + $this->assertInstanceOf(UploadedFileInterface::class, $files['file']); /* @var $file \Psr\Http\Message\UploadedFileInterface */ $file = $files['file']; @@ -708,7 +709,7 @@ public function testInvalidUploadFileWithoutMultipleContentTypeUsesLastValue() $this->assertCount(1, $files); $this->assertTrue(isset($files['file'])); - $this->assertInstanceOf('Psr\Http\Message\UploadedFileInterface', $files['file']); + $this->assertInstanceOf(UploadedFileInterface::class, $files['file']); /* @var $file \Psr\Http\Message\UploadedFileInterface */ $file = $files['file']; @@ -742,7 +743,7 @@ public function testUploadEmptyFile() $this->assertCount(1, $files); $this->assertTrue(isset($files['file'])); - $this->assertInstanceOf('Psr\Http\Message\UploadedFileInterface', $files['file']); + $this->assertInstanceOf(UploadedFileInterface::class, $files['file']); /* @var $file \Psr\Http\Message\UploadedFileInterface */ $file = $files['file']; @@ -776,7 +777,7 @@ public function testUploadTooLargeFile() $this->assertCount(1, $files); $this->assertTrue(isset($files['file'])); - $this->assertInstanceOf('Psr\Http\Message\UploadedFileInterface', $files['file']); + $this->assertInstanceOf(UploadedFileInterface::class, $files['file']); /* @var $file \Psr\Http\Message\UploadedFileInterface */ $file = $files['file']; @@ -809,7 +810,7 @@ public function testUploadTooLargeFileWithIniLikeSize() $this->assertCount(1, $files); $this->assertTrue(isset($files['file'])); - $this->assertInstanceOf('Psr\Http\Message\UploadedFileInterface', $files['file']); + $this->assertInstanceOf(UploadedFileInterface::class, $files['file']); /* @var $file \Psr\Http\Message\UploadedFileInterface */ $file = $files['file']; @@ -842,7 +843,7 @@ public function testUploadNoFile() $this->assertCount(1, $files); $this->assertTrue(isset($files['file'])); - $this->assertInstanceOf('Psr\Http\Message\UploadedFileInterface', $files['file']); + $this->assertInstanceOf(UploadedFileInterface::class, $files['file']); /* @var $file \Psr\Http\Message\UploadedFileInterface */ $file = $files['file']; @@ -1046,7 +1047,7 @@ public function testWeOnlyParseTheAmountOfMultiPartChunksWeConfigured() $parser = new MultipartParser(); - $reflectecClass = new \ReflectionClass('\React\Http\Io\MultipartParser'); + $reflectecClass = new \ReflectionClass(MultipartParser::class); $requestProperty = $reflectecClass->getProperty('request'); $requestProperty->setAccessible(true); $cursorProperty = $reflectecClass->getProperty('cursor'); diff --git a/tests/Io/PauseBufferStreamTest.php b/tests/Io/PauseBufferStreamTest.php index 05bf3ee3..139db8fd 100644 --- a/tests/Io/PauseBufferStreamTest.php +++ b/tests/Io/PauseBufferStreamTest.php @@ -2,15 +2,16 @@ namespace React\Tests\Io; -use React\Tests\Http\TestCase; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; +use React\Tests\Http\TestCase; use React\Http\Io\PauseBufferStream; class PauseBufferStreamTest extends TestCase { public function testPauseMethodWillBePassedThroughToInput() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('pause'); $stream = new PauseBufferStream($input); @@ -19,7 +20,7 @@ public function testPauseMethodWillBePassedThroughToInput() public function testCloseMethodWillBePassedThroughToInput() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->once())->method('close'); $stream = new PauseBufferStream($input); @@ -28,7 +29,7 @@ public function testCloseMethodWillBePassedThroughToInput() public function testPauseMethodWillNotBePassedThroughToInputAfterClose() { - $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $input = $this->createMock(ReadableStreamInterface::class); $input->expects($this->never())->method('pause'); $stream = new PauseBufferStream($input); diff --git a/tests/Io/ReadableBodyStreamTest.php b/tests/Io/ReadableBodyStreamTest.php index 8ece6791..2409a6be 100644 --- a/tests/Io/ReadableBodyStreamTest.php +++ b/tests/Io/ReadableBodyStreamTest.php @@ -3,8 +3,9 @@ namespace React\Tests\Http\Io; use React\Http\Io\ReadableBodyStream; -use React\Tests\Http\TestCase; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; +use React\Tests\Http\TestCase; class ReadableBodyStreamTest extends TestCase { @@ -16,7 +17,7 @@ class ReadableBodyStreamTest extends TestCase */ public function setUpStream() { - $this->input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $this->input = $this->createMock(ReadableStreamInterface::class); $this->stream = new ReadableBodyStream($this->input); } @@ -102,7 +103,7 @@ public function testEndInputWillEmitErrorEventWhenDataDoesNotReachExpectedLength $this->input->write('hi'); $this->input->end(); - $this->assertInstanceOf('UnderflowException', $called); + $this->assertInstanceOf(\UnderflowException::class, $called); $this->assertSame('Unexpected end of response body after 2/5 bytes', $called->getMessage()); } @@ -188,7 +189,7 @@ public function testPointlessTostringReturnsEmptyString() public function testPointlessDetachThrows() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->stream->detach(); } @@ -199,7 +200,7 @@ public function testPointlessGetSizeReturnsNull() public function testPointlessTellThrows() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->stream->tell(); } @@ -210,13 +211,13 @@ public function testPointlessIsSeekableReturnsFalse() public function testPointlessSeekThrows() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->stream->seek(0); } public function testPointlessRewindThrows() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->stream->rewind(); } @@ -227,19 +228,19 @@ public function testPointlessIsWritableReturnsFalse() public function testPointlessWriteThrows() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->stream->write(''); } public function testPointlessReadThrows() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->stream->read(8192); } public function testPointlessGetContentsThrows() { - $this->setExpectedException('BadMethodCallException'); + $this->expectException(\BadMethodCallException::class); $this->stream->getContents(); } diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index d15d4e7f..568fc375 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -2,20 +2,24 @@ namespace React\Tests\Http\Io; +use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ServerRequestInterface; +use React\Http\Io\Clock; use React\Http\Io\RequestHeaderParser; +use React\Socket\Connection; +use React\Stream\ReadableStreamInterface; use React\Tests\Http\TestCase; class RequestHeaderParserTest extends TestCase { public function testSplitShouldHappenOnDoubleCrlf() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); @@ -31,12 +35,12 @@ public function testSplitShouldHappenOnDoubleCrlf() public function testFeedInOneGo() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableOnce()); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $data = $this->createGetRequest(); @@ -45,7 +49,7 @@ public function testFeedInOneGo() public function testFeedTwoRequestsOnSeparateConnections() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); @@ -54,8 +58,8 @@ public function testFeedTwoRequestsOnSeparateConnections() ++$called; }); - $connection1 = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); - $connection2 = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection1 = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection2 = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection1); $parser->handle($connection2); @@ -71,7 +75,7 @@ public function testHeadersEventShouldEmitRequestAndConnection() $request = null; $conn = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', function ($parsedRequest, $connection) use (&$request, &$conn) { @@ -79,13 +83,13 @@ public function testHeadersEventShouldEmitRequestAndConnection() $conn = $connection; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $data = $this->createGetRequest(); $connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $request); + $this->assertInstanceOf(RequestInterface::class, $request); $this->assertSame('GET', $request->getMethod()); $this->assertEquals('http://example.com/', $request->getUri()); $this->assertSame('1.1', $request->getProtocolVersion()); @@ -96,21 +100,21 @@ public function testHeadersEventShouldEmitRequestAndConnection() public function testHeadersEventShouldEmitRequestWhichShouldEmitEndForStreamingBodyWithoutContentLengthFromInitialRequestBody() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $ended = false; $parser->on('headers', function (ServerRequestInterface $request) use (&$ended) { $body = $request->getBody(); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); $body->on('end', function () use (&$ended) { $ended = true; }); }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $data = "GET / HTTP/1.0\r\n\r\n"; @@ -121,14 +125,14 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitEndForStreamingB public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDataFromInitialRequestBody() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $buffer = ''; $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer) { $body = $request->getBody(); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); $body->on('data', function ($chunk) use (&$buffer) { $buffer .= $chunk; @@ -138,7 +142,7 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDat }); }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $data = "POST / HTTP/1.0\r\nContent-Length: 11\r\n\r\n"; @@ -150,21 +154,21 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDat public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyWithPlentyOfDataFromInitialRequestBody() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $buffer = ''; $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer) { $body = $request->getBody(); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); $body->on('data', function ($chunk) use (&$buffer) { $buffer .= $chunk; }); }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $size = 10000; @@ -177,21 +181,21 @@ public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyWit public function testHeadersEventShouldEmitRequestWhichShouldNotEmitStreamingBodyDataWithoutContentLengthFromInitialRequestBody() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $buffer = ''; $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer) { $body = $request->getBody(); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); $body->on('data', function ($chunk) use (&$buffer) { $buffer .= $chunk; }); }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $data = "POST / HTTP/1.0\r\n\r\n"; @@ -203,21 +207,21 @@ public function testHeadersEventShouldEmitRequestWhichShouldNotEmitStreamingBody public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDataUntilContentLengthBoundaryFromInitialRequestBody() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $buffer = ''; $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer) { $body = $request->getBody(); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); $body->on('data', function ($chunk) use (&$buffer) { $buffer .= $chunk; }); }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $data = "POST / HTTP/1.0\r\nContent-Length: 6\r\n\r\n"; @@ -231,20 +235,20 @@ public function testHeadersEventShouldParsePathAndQueryString() { $request = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $data = $this->createAdvancedPostRequest(); $connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $request); + $this->assertInstanceOf(RequestInterface::class, $request); $this->assertSame('POST', $request->getMethod()); $this->assertEquals('http://example.com/foo?bar=baz', $request->getUri()); $this->assertSame('1.1', $request->getProtocolVersion()); @@ -260,14 +264,14 @@ public function testHeaderEventWithShouldApplyDefaultAddressFromLocalConnectionA { $request = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress'])->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(['getLocalAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('tcp://127.1.1.1:8000'); $parser->handle($connection); @@ -281,14 +285,14 @@ public function testHeaderEventViaHttpsShouldApplyHttpsSchemeFromLocalTlsConnect { $request = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress'])->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(['getLocalAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('tls://127.1.1.1:8000'); $parser->handle($connection); @@ -303,7 +307,7 @@ public function testHeaderOverflowShouldEmitError() $error = null; $passedConnection = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -312,13 +316,13 @@ public function testHeaderOverflowShouldEmitError() $passedConnection = $connection; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $data = str_repeat('A', 8193); $connection->emit('data', [$data]); - $this->assertInstanceOf('OverflowException', $error); + $this->assertInstanceOf(\OverflowException::class, $error); $this->assertSame('Maximum header size of 8192 exceeded.', $error->getMessage()); $this->assertSame($connection, $passedConnection); } @@ -327,7 +331,7 @@ public function testInvalidEmptyRequestHeadersParseException() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -335,12 +339,12 @@ public function testInvalidEmptyRequestHeadersParseException() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('Unable to parse invalid request-line', $error->getMessage()); } @@ -348,7 +352,7 @@ public function testInvalidMalformedRequestLineParseException() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -356,12 +360,12 @@ public function testInvalidMalformedRequestLineParseException() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET /\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('Unable to parse invalid request-line', $error->getMessage()); } @@ -369,7 +373,7 @@ public function testInvalidMalformedRequestHeadersThrowsParseException() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -377,12 +381,12 @@ public function testInvalidMalformedRequestHeadersThrowsParseException() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET / HTTP/1.1\r\nHost : yes\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('Unable to parse invalid request header fields', $error->getMessage()); } @@ -390,7 +394,7 @@ public function testInvalidMalformedRequestHeadersWhitespaceThrowsParseException { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -398,12 +402,12 @@ public function testInvalidMalformedRequestHeadersWhitespaceThrowsParseException $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET / HTTP/1.1\r\nHost: yes\rFoo: bar\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('Unable to parse invalid request header fields', $error->getMessage()); } @@ -411,7 +415,7 @@ public function testInvalidAbsoluteFormSchemeEmitsError() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -419,12 +423,12 @@ public function testInvalidAbsoluteFormSchemeEmitsError() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET tcp://example.com:80/ HTTP/1.0\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); } @@ -432,7 +436,7 @@ public function testOriginFormWithSchemeSeparatorInParam() { $request = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('error', $this->expectCallableNever()); @@ -440,12 +444,12 @@ public function testOriginFormWithSchemeSeparatorInParam() $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET /somepath?param=http://example.com HTTP/1.1\r\nHost: localhost\r\n\r\n"]); - $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $request); + $this->assertInstanceOf(RequestInterface::class, $request); $this->assertSame('GET', $request->getMethod()); $this->assertEquals('http://localhost/somepath?param=http://example.com', $request->getUri()); $this->assertSame('1.1', $request->getProtocolVersion()); @@ -459,7 +463,7 @@ public function testUriStartingWithColonSlashSlashFails() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -467,12 +471,12 @@ public function testUriStartingWithColonSlashSlashFails() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET ://example.com:80/ HTTP/1.0\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); } @@ -480,7 +484,7 @@ public function testInvalidAbsoluteFormWithFragmentEmitsError() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -488,12 +492,12 @@ public function testInvalidAbsoluteFormWithFragmentEmitsError() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET http://example.com:80/#home HTTP/1.0\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('Invalid absolute-form request-target', $error->getMessage()); } @@ -501,7 +505,7 @@ public function testInvalidHeaderContainsFullUri() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -509,12 +513,12 @@ public function testInvalidHeaderContainsFullUri() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET / HTTP/1.1\r\nHost: http://user:pass@host/\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('Invalid Host header value', $error->getMessage()); } @@ -522,7 +526,7 @@ public function testInvalidAbsoluteFormWithHostHeaderEmpty() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -530,12 +534,12 @@ public function testInvalidAbsoluteFormWithHostHeaderEmpty() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET http://example.com/ HTTP/1.1\r\nHost: \r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('Invalid Host header value', $error->getMessage()); } @@ -543,7 +547,7 @@ public function testInvalidConnectRequestWithNonAuthorityForm() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -551,12 +555,12 @@ public function testInvalidConnectRequestWithNonAuthorityForm() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["CONNECT http://example.com:8080/ HTTP/1.1\r\nHost: example.com:8080\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame('CONNECT method MUST use authority-form request target', $error->getMessage()); } @@ -564,7 +568,7 @@ public function testInvalidHttpVersion() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -572,12 +576,12 @@ public function testInvalidHttpVersion() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET / HTTP/1.2\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame(505, $error->getCode()); $this->assertSame('Received request with invalid protocol version', $error->getMessage()); } @@ -586,7 +590,7 @@ public function testInvalidContentLengthRequestHeaderWillEmitError() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -594,12 +598,12 @@ public function testInvalidContentLengthRequestHeaderWillEmitError() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: foo\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame(400, $error->getCode()); $this->assertSame('The value of `Content-Length` is not valid', $error->getMessage()); } @@ -608,7 +612,7 @@ public function testInvalidRequestWithMultipleContentLengthRequestHeadersWillEmi { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -616,12 +620,12 @@ public function testInvalidRequestWithMultipleContentLengthRequestHeadersWillEmi $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 4\r\nContent-Length: 5\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame(400, $error->getCode()); $this->assertSame('The value of `Content-Length` is not valid', $error->getMessage()); } @@ -630,7 +634,7 @@ public function testInvalidTransferEncodingRequestHeaderWillEmitError() { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -638,12 +642,12 @@ public function testInvalidTransferEncodingRequestHeaderWillEmitError() $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: foo\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame(501, $error->getCode()); $this->assertSame('Only chunked-encoding is allowed for Transfer-Encoding', $error->getMessage()); } @@ -652,7 +656,7 @@ public function testInvalidRequestWithBothTransferEncodingAndContentLengthWillEm { $error = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); $parser->on('headers', $this->expectCallableNever()); @@ -660,12 +664,12 @@ public function testInvalidRequestWithBothTransferEncodingAndContentLengthWillEm $error = $message; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\nContent-Length: 0\r\n\r\n"]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); $this->assertSame(400, $error->getCode()); $this->assertSame('Using both `Transfer-Encoding: chunked` and `Content-Length` is not allowed', $error->getMessage()); } @@ -674,7 +678,7 @@ public function testServerParamsWillBeSetOnHttpsRequest() { $request = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $clock->expects($this->once())->method('now')->willReturn(1652972091.3958); $parser = new RequestHeaderParser($clock); @@ -683,7 +687,7 @@ public function testServerParamsWillBeSetOnHttpsRequest() $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('tls://127.1.1.1:8000'); $connection->expects($this->once())->method('getRemoteAddress')->willReturn('tls://192.168.1.1:8001'); $parser->handle($connection); @@ -707,7 +711,7 @@ public function testServerParamsWillBeSetOnHttpRequest() { $request = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $clock->expects($this->once())->method('now')->willReturn(1652972091.3958); $parser = new RequestHeaderParser($clock); @@ -716,7 +720,7 @@ public function testServerParamsWillBeSetOnHttpRequest() $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(['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); @@ -740,7 +744,7 @@ public function testServerParamsWillNotSetRemoteAddressForUnixDomainSockets() { $request = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $clock->expects($this->once())->method('now')->willReturn(1652972091.3958); $parser = new RequestHeaderParser($clock); @@ -749,7 +753,7 @@ public function testServerParamsWillNotSetRemoteAddressForUnixDomainSockets() $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); $connection->expects($this->once())->method('getLocalAddress')->willReturn('unix://./server.sock'); $connection->expects($this->once())->method('getRemoteAddress')->willReturn(null); $parser->handle($connection); @@ -773,7 +777,7 @@ public function testServerParamsWontBeSetOnMissingUrls() { $request = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $clock->expects($this->once())->method('now')->willReturn(1652972091.3958); $parser = new RequestHeaderParser($clock); @@ -782,7 +786,7 @@ public function testServerParamsWontBeSetOnMissingUrls() $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); @@ -801,12 +805,12 @@ public function testServerParamsWontBeSetOnMissingUrls() public function testServerParamsWillBeReusedForMultipleRequestsFromSameConnection() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $clock->expects($this->exactly(2))->method('now')->willReturn(1652972091.3958); $parser = new RequestHeaderParser($clock); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(['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'); @@ -837,11 +841,11 @@ public function testServerParamsWillBeReusedForMultipleRequestsFromSameConnectio public function testServerParamsWillBeRememberedUntilConnectionIsClosed() { - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(['getLocalAddress', 'getRemoteAddress'])->getMock(); $parser->handle($connection); $connection->emit('data', ["GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n"]); @@ -859,7 +863,7 @@ public function testQueryParmetersWillBeSet() { $request = null; - $clock = $this->getMockBuilder('React\Http\Io\Clock')->disableOriginalConstructor()->getMock(); + $clock = $this->createMock(Clock::class); $parser = new RequestHeaderParser($clock); @@ -867,7 +871,7 @@ public function testQueryParmetersWillBeSet() $request = $parsedRequest; }); - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); $connection->emit('data', ["GET /foo.php?hello=world&test=this HTTP/1.0\r\nHost: example.com\r\n\r\n"]); diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index 59bb9719..df4c5359 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -3,13 +3,17 @@ namespace React\Tests\Http\Io; use Psr\Http\Message\RequestInterface; +use React\EventLoop\LoopInterface; use React\Http\Client\Client as HttpClient; use React\Http\Io\ClientConnectionManager; +use React\Http\Io\ClientRequestStream; use React\Http\Io\EmptyBodyStream; use React\Http\Io\ReadableBodyStream; use React\Http\Io\Sender; use React\Http\Message\Request; use React\Promise\Promise; +use React\Socket\ConnectionInterface; +use React\Socket\ConnectorInterface; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; use function React\Promise\reject; @@ -25,19 +29,19 @@ class SenderTest extends TestCase */ public function setUpLoop() { - $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $this->loop = $this->createMock(LoopInterface::class); } public function testCreateFromLoop() { $sender = Sender::createFromLoop($this->loop, null); - $this->assertInstanceOf('React\Http\Io\Sender', $sender); + $this->assertInstanceOf(Sender::class, $sender); } public function testSenderRejectsInvalidUri() { - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->never())->method('connect'); $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); @@ -51,12 +55,12 @@ public function testSenderRejectsInvalidUri() $exception = $e; }); - $this->assertInstanceOf('InvalidArgumentException', $exception); + $this->assertInstanceOf(\InvalidArgumentException::class, $exception); } public function testSenderConnectorRejection() { - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->willReturn(reject(new \RuntimeException('Rejected'))); $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); @@ -70,15 +74,15 @@ public function testSenderConnectorRejection() $exception = $e; }); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); } public function testSendPostWillAutomaticallySendContentLengthHeader() { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $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()); + }))->willReturn($this->createMock(ClientRequestStream::class)); $sender = new Sender($client); @@ -88,10 +92,10 @@ public function testSendPostWillAutomaticallySendContentLengthHeader() public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmptyRequestBody() { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $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()); + }))->willReturn($this->createMock(ClientRequestStream::class)); $sender = new Sender($client); @@ -101,10 +105,10 @@ public function testSendPostWillAutomaticallySendContentLengthZeroHeaderForEmpty public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() { - $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); + $outgoing = $this->createMock(ClientRequestStream::class); $outgoing->expects($this->once())->method('write')->with(""); - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->with($this->callback(function (RequestInterface $request) { return $request->getHeaderLine('Transfer-Encoding') === 'chunked'; }))->willReturn($outgoing); @@ -118,11 +122,11 @@ public function testSendPostStreamWillAutomaticallySendTransferEncodingChunked() public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAndRespectRequestThrottling() { - $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); + $outgoing = $this->createMock(ClientRequestStream::class); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->exactly(2))->method('write')->withConsecutive([""], ["5\r\nhello\r\n"])->willReturn(false); - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client); @@ -137,12 +141,12 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForWriteAn public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() { - $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); + $outgoing = $this->createMock(ClientRequestStream::class); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->exactly(2))->method('write')->withConsecutive([""], ["0\r\n\r\n"])->willReturn(false); $outgoing->expects($this->once())->method('end')->with(null); - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client); @@ -156,13 +160,13 @@ public function testSendPostStreamWillAutomaticallyPipeChunkEncodeBodyForEnd() public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() { - $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); + $outgoing = $this->createMock(ClientRequestStream::class); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->once())->method('write')->with("")->willReturn(false); $outgoing->expects($this->never())->method('end'); $outgoing->expects($this->once())->method('close'); - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client); @@ -179,20 +183,20 @@ public function testSendPostStreamWillRejectWhenRequestBodyEmitsErrorEvent() $exception = $e; }); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); $this->assertEquals('Request failed because request body reported an error', $exception->getMessage()); $this->assertSame($expected, $exception->getPrevious()); } public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() { - $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); + $outgoing = $this->createMock(ClientRequestStream::class); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->once())->method('write')->with("")->willReturn(false); $outgoing->expects($this->never())->method('end'); $outgoing->expects($this->once())->method('close'); - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client); @@ -208,19 +212,19 @@ public function testSendPostStreamWillRejectWhenRequestBodyClosesWithoutEnd() $exception = $e; }); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); $this->assertEquals('Request failed because request body closed unexpectedly', $exception->getMessage()); } public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() { - $outgoing = $this->getMockBuilder('React\Http\Io\ClientRequestStream')->disableOriginalConstructor()->getMock(); + $outgoing = $this->createMock(ClientRequestStream::class); $outgoing->expects($this->once())->method('isWritable')->willReturn(true); $outgoing->expects($this->exactly(2))->method('write')->withConsecutive([""], ["0\r\n\r\n"])->willReturn(false); $outgoing->expects($this->once())->method('end'); $outgoing->expects($this->never())->method('close'); - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $client->expects($this->once())->method('request')->willReturn($outgoing); $sender = new Sender($client); @@ -242,10 +246,10 @@ public function testSendPostStreamWillNotRejectWhenRequestBodyClosesAfterEnd() public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $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()); + }))->willReturn($this->createMock(ClientRequestStream::class)); $sender = new Sender($client); @@ -256,10 +260,10 @@ public function testSendPostStreamWithExplicitContentLengthWillSendHeaderAsIs() public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $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()); + }))->willReturn($this->createMock(ClientRequestStream::class)); $sender = new Sender($client); @@ -269,10 +273,10 @@ public function testSendGetWillNotPassContentLengthHeaderForEmptyRequestBody() public function testSendGetWithEmptyBodyStreamWillNotPassContentLengthOrTransferEncodingHeader() { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $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()); + }))->willReturn($this->createMock(ClientRequestStream::class)); $sender = new Sender($client); @@ -284,10 +288,10 @@ public function testSendGetWithEmptyBodyStreamWillNotPassContentLengthOrTransfer public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyRequestBody() { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $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()); + }))->willReturn($this->createMock(ClientRequestStream::class)); $sender = new Sender($client); @@ -297,10 +301,10 @@ public function testSendCustomMethodWillNotPassContentLengthHeaderForEmptyReques public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsIs() { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $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()); + }))->willReturn($this->createMock(ClientRequestStream::class)); $sender = new Sender($client); @@ -311,10 +315,10 @@ public function testSendCustomMethodWithExplicitContentLengthZeroWillBePassedAsI /** @test */ public function getRequestWithUserAndPassShouldSendAGetRequestWithBasicAuthorizationHeader() { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $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()); + }))->willReturn($this->createMock(ClientRequestStream::class)); $sender = new Sender($client); @@ -325,10 +329,10 @@ public function getRequestWithUserAndPassShouldSendAGetRequestWithBasicAuthoriza /** @test */ public function getRequestWithUserAndPassShouldSendAGetRequestWithGivenAuthorizationHeaderBasicAuthorizationHeader() { - $client = $this->getMockBuilder('React\Http\Client\Client')->disableOriginalConstructor()->getMock(); + $client = $this->createMock(HttpClient::class); $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()); + }))->willReturn($this->createMock(ClientRequestStream::class)); $sender = new Sender($client); @@ -342,7 +346,7 @@ public function testCancelRequestWillCancelConnector() throw new \RuntimeException(); }); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->willReturn($promise); $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); @@ -357,15 +361,15 @@ public function testCancelRequestWillCancelConnector() $exception = $e; }); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); } public function testCancelRequestWillCloseConnection() { - $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection = $this->createMock(ConnectionInterface::class); $connection->expects($this->once())->method('close'); - $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector = $this->createMock(ConnectorInterface::class); $connector->expects($this->once())->method('connect')->willReturn(resolve($connection)); $sender = new Sender(new HttpClient(new ClientConnectionManager($connector, $this->loop))); @@ -380,6 +384,6 @@ public function testCancelRequestWillCloseConnection() $exception = $e; }); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); } } diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index 3b5f28f4..a61d1425 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -4,11 +4,14 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamInterface; use React\EventLoop\Loop; +use React\Http\Io\RequestHeaderParser; use React\Http\Io\StreamingServer; use React\Http\Message\Response; use React\Http\Message\ServerRequest; use React\Promise\Promise; +use React\Socket\Connection; use React\Stream\ThroughStream; use React\Tests\Http\SocketServerStub; use React\Tests\Http\TestCase; @@ -35,7 +38,7 @@ public function setUpConnectionMockAndSocket() private function mockConnection(array $additionalMethods = null) { - $connection = $this->getMockBuilder('React\Socket\Connection') + $connection = $this->getMockBuilder(Connection::class) ->disableOriginalConstructor() ->setMethods(array_merge( [ @@ -125,7 +128,7 @@ public function testRequestEvent() $serverParams = $requestAssertion->getServerParams(); $this->assertSame(1, $i); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); @@ -158,7 +161,7 @@ public function testRequestEventWithSingleRequestHandlerArray() $serverParams = $requestAssertion->getServerParams(); $this->assertSame(1, $i); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); @@ -181,7 +184,7 @@ public function testRequestGetWithHostAndCustomPort() $data = "GET / HTTP/1.1\r\nHost: example.com:8080\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); @@ -203,7 +206,7 @@ public function testRequestGetWithHostAndHttpsPort() $data = "GET / HTTP/1.1\r\nHost: example.com:443\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); @@ -225,7 +228,7 @@ public function testRequestGetWithHostAndDefaultPortWillBeIgnored() $data = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); @@ -247,7 +250,7 @@ public function testRequestGetHttp10WithoutHostWillBeIgnored() $data = "GET / HTTP/1.0\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/', $requestAssertion->getRequestTarget()); $this->assertSame('/', $requestAssertion->getUri()->getPath()); @@ -282,7 +285,7 @@ public function testRequestOptionsAsterisk() $data = "OPTIONS * HTTP/1.1\r\nHost: example.com\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('OPTIONS', $requestAssertion->getMethod()); $this->assertSame('*', $requestAssertion->getRequestTarget()); $this->assertSame('', $requestAssertion->getUri()->getPath()); @@ -315,7 +318,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', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); $this->assertSame('example.com:443', $requestAssertion->getRequestTarget()); $this->assertSame('', $requestAssertion->getUri()->getPath()); @@ -337,7 +340,7 @@ public function testRequestConnectWithoutHostWillBePassesAsIs() $data = "CONNECT example.com:443 HTTP/1.1\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); $this->assertSame('example.com:443', $requestAssertion->getRequestTarget()); $this->assertSame('', $requestAssertion->getUri()->getPath()); @@ -359,7 +362,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', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); $this->assertSame('example.com:80', $requestAssertion->getRequestTarget()); $this->assertSame('', $requestAssertion->getUri()->getPath()); @@ -381,7 +384,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', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('CONNECT', $requestAssertion->getMethod()); $this->assertSame('example.com:80', $requestAssertion->getRequestTarget()); $this->assertSame('', $requestAssertion->getUri()->getPath()); @@ -433,7 +436,7 @@ public function testRequestWithoutHostEventUsesSocketAddress() $data = "GET /test HTTP/1.0\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('/test', $requestAssertion->getRequestTarget()); $this->assertEquals('http://127.0.0.1/test', $requestAssertion->getUri()); @@ -454,7 +457,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', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('http://example.com/test', $requestAssertion->getRequestTarget()); $this->assertEquals('http://example.com/test', $requestAssertion->getUri()); @@ -476,7 +479,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', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('GET', $requestAssertion->getMethod()); $this->assertSame('http://example.com/test', $requestAssertion->getRequestTarget()); $this->assertEquals('http://example.com/test', $requestAssertion->getUri()); @@ -510,7 +513,7 @@ public function testRequestOptionsAsteriskEvent() $data = "OPTIONS * HTTP/1.1\r\nHost: example.com\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('OPTIONS', $requestAssertion->getMethod()); $this->assertSame('*', $requestAssertion->getRequestTarget()); $this->assertEquals('http://example.com/', $requestAssertion->getUri()); @@ -532,7 +535,7 @@ public function testRequestOptionsAbsoluteEvent() $data = "OPTIONS http://example.com HTTP/1.1\r\nHost: example.com\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $requestAssertion); + $this->assertInstanceOf(ServerRequestInterface::class, $requestAssertion); $this->assertSame('OPTIONS', $requestAssertion->getMethod()); $this->assertSame('http://example.com/', $requestAssertion->getRequestTarget()); $this->assertEquals('http://example.com/', $requestAssertion->getUri()); @@ -698,12 +701,10 @@ public function testResponseContainsServerHeader() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -712,7 +713,7 @@ function ($data) use (&$buffer) { $data = $this->createGetRequest(); $this->connection->emit('data', [$data]); - $this->assertContainsString("\r\nServer: ReactPHP/1\r\n", $buffer); + $this->assertStringContainsString("\r\nServer: ReactPHP/1\r\n", $buffer); } public function testResponsePendingPromiseWillNotSendAnything() @@ -728,12 +729,10 @@ public function testResponsePendingPromiseWillNotSendAnything() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -758,12 +757,10 @@ public function testResponsePendingPromiseWillBeCancelledIfConnectionCloses() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -794,12 +791,10 @@ public function testResponseBodyStreamAlreadyClosedWillSendEmptyBodyChunkedEncod $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -829,12 +824,10 @@ public function testResponseBodyStreamEndingWillSendEmptyBodyChunkedEncoded() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -867,12 +860,10 @@ public function testResponseBodyStreamAlreadyClosedWillSendEmptyBodyPlainHttp10( $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -903,15 +894,13 @@ public function testResponseStreamWillBeClosedIfConnectionIsAlreadyClosed() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); - $this->connection = $this->getMockBuilder('React\Socket\Connection') + $this->connection = $this->getMockBuilder(Connection::class) ->disableOriginalConstructor() ->setMethods( [ @@ -980,12 +969,10 @@ public function testResponseUpgradeInResponseCanBeUsedToAdvertisePossibleUpgrade $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1015,12 +1002,10 @@ public function testResponseUpgradeWishInRequestCanBeIgnoredByReturningNormalRes $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1053,12 +1038,10 @@ public function testResponseUpgradeSwitchingProtocolIncludesConnectionUpgradeHea $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1091,12 +1074,10 @@ public function testResponseUpgradeSwitchingProtocolWithStreamWillPipeDataToConn $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1128,12 +1109,10 @@ public function testResponseConnectMethodStreamWillPipeDataToConnection() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1185,12 +1164,10 @@ public function testResponseContainsSameRequestProtocolVersionAndChunkedBodyForH $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1199,8 +1176,8 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("bye", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("bye", $buffer); } public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp10() @@ -1218,12 +1195,10 @@ public function testResponseContainsSameRequestProtocolVersionAndRawBodyForHttp1 $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1232,9 +1207,9 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.0\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.0 200 OK\r\n", $buffer); - $this->assertContainsString("\r\n\r\n", $buffer); - $this->assertContainsString("bye", $buffer); + $this->assertStringContainsString("HTTP/1.0 200 OK\r\n", $buffer); + $this->assertStringContainsString("\r\n\r\n", $buffer); + $this->assertStringContainsString("bye", $buffer); } public function testResponseContainsNoResponseBodyForHeadRequest() @@ -1251,12 +1226,10 @@ public function testResponseContainsNoResponseBodyForHeadRequest() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1265,9 +1238,9 @@ function ($data) use (&$buffer) { $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); - $this->assertNotContainsString("bye", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("\r\nContent-Length: 3\r\n", $buffer); + $this->assertStringNotContainsString("bye", $buffer); } public function testResponseContainsNoResponseBodyForHeadRequestWithStreamingResponse() @@ -1287,12 +1260,10 @@ public function testResponseContainsNoResponseBodyForHeadRequestWithStreamingRes $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1301,8 +1272,8 @@ function ($data) use (&$buffer) { $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("\r\nContent-Length: 3\r\n", $buffer); } public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContentStatus() @@ -1319,12 +1290,10 @@ public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContent $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1333,9 +1302,9 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 204 No Content\r\n", $buffer); - $this->assertNotContainsString("\r\nContent-Length: 3\r\n", $buffer); - $this->assertNotContainsString("bye", $buffer); + $this->assertStringContainsString("HTTP/1.1 204 No Content\r\n", $buffer); + $this->assertStringNotContainsString("\r\nContent-Length: 3\r\n", $buffer); + $this->assertStringNotContainsString("bye", $buffer); } public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContentStatusResponseWithStreamingBody() @@ -1355,12 +1324,10 @@ public function testResponseContainsNoResponseBodyAndNoContentLengthForNoContent $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1369,8 +1336,8 @@ function ($data) use (&$buffer) { $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 204 No Content\r\n", $buffer); - $this->assertNotContainsString("\r\nContent-Length: 3\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 204 No Content\r\n", $buffer); + $this->assertStringNotContainsString("\r\nContent-Length: 3\r\n", $buffer); } public function testResponseContainsNoContentLengthHeaderForNotModifiedStatus() @@ -1387,12 +1354,10 @@ public function testResponseContainsNoContentLengthHeaderForNotModifiedStatus() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1401,8 +1366,8 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); - $this->assertNotContainsString("\r\nContent-Length: 0\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); + $this->assertStringNotContainsString("\r\nContent-Length: 0\r\n", $buffer); } public function testResponseContainsExplicitContentLengthHeaderForNotModifiedStatus() @@ -1419,12 +1384,10 @@ public function testResponseContainsExplicitContentLengthHeaderForNotModifiedSta $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1433,8 +1396,8 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); - $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); + $this->assertStringContainsString("\r\nContent-Length: 3\r\n", $buffer); } public function testResponseContainsExplicitContentLengthHeaderForHeadRequests() @@ -1451,12 +1414,10 @@ public function testResponseContainsExplicitContentLengthHeaderForHeadRequests() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1465,8 +1426,8 @@ function ($data) use (&$buffer) { $data = "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("\r\nContent-Length: 3\r\n", $buffer); } public function testResponseContainsNoResponseBodyForNotModifiedStatus() @@ -1483,12 +1444,10 @@ public function testResponseContainsNoResponseBodyForNotModifiedStatus() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1497,9 +1456,9 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); - $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); - $this->assertNotContainsString("bye", $buffer); + $this->assertStringContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); + $this->assertStringContainsString("\r\nContent-Length: 3\r\n", $buffer); + $this->assertStringNotContainsString("bye", $buffer); } public function testResponseContainsNoResponseBodyForNotModifiedStatusWithStreamingBody() @@ -1519,12 +1478,10 @@ public function testResponseContainsNoResponseBodyForNotModifiedStatusWithStream $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1533,8 +1490,8 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); - $this->assertContainsString("\r\nContent-Length: 3\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 304 Not Modified\r\n", $buffer); + $this->assertStringContainsString("\r\nContent-Length: 3\r\n", $buffer); } public function testRequestInvalidHttpProtocolVersionWillEmitErrorAndSendErrorResponse() @@ -1550,12 +1507,10 @@ public function testRequestInvalidHttpProtocolVersionWillEmitErrorAndSendErrorRe $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1564,11 +1519,11 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.2\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); - $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->assertStringContainsString("HTTP/1.1 505 HTTP Version Not Supported\r\n", $buffer); + $this->assertStringContainsString("\r\n\r\n", $buffer); + $this->assertStringContainsString("Error 505: HTTP Version Not Supported", $buffer); } public function testRequestOverflowWillEmitErrorAndSendErrorResponse() @@ -1584,12 +1539,10 @@ public function testRequestOverflowWillEmitErrorAndSendErrorResponse() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1599,10 +1552,10 @@ function ($data) use (&$buffer) { $data .= str_repeat('A', 8193 - strlen($data)) . "\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('OverflowException', $error); + $this->assertInstanceOf(\OverflowException::class, $error); - $this->assertContainsString("HTTP/1.1 431 Request Header Fields Too Large\r\n", $buffer); - $this->assertContainsString("\r\n\r\nError 431: Request Header Fields Too Large", $buffer); + $this->assertStringContainsString("HTTP/1.1 431 Request Header Fields Too Large\r\n", $buffer); + $this->assertStringContainsString("\r\n\r\nError 431: Request Header Fields Too Large", $buffer); } public function testRequestInvalidWillEmitErrorAndSendErrorResponse() @@ -1618,12 +1571,10 @@ public function testRequestInvalidWillEmitErrorAndSendErrorResponse() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -1632,10 +1583,10 @@ function ($data) use (&$buffer) { $data = "bad request\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertInstanceOf(\InvalidArgumentException::class, $error); - $this->assertContainsString("HTTP/1.1 400 Bad Request\r\n", $buffer); - $this->assertContainsString("\r\n\r\nError 400: Bad Request", $buffer); + $this->assertStringContainsString("HTTP/1.1 400 Bad Request\r\n", $buffer); + $this->assertStringContainsString("\r\n\r\nError 400: Bad Request", $buffer); } public function testRequestContentLengthBodyDataWillEmitDataEventOnRequestStream() @@ -1959,7 +1910,7 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream() { - $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); + $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf(\Exception::class)); $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); return resolve(new Response()); @@ -1984,7 +1935,7 @@ public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() { - $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); + $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf(\Exception::class)); $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -2006,7 +1957,7 @@ public function testRequestInvalidChunkBodyTooLongWillEmitErrorOnRequestStream() public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWillEmitErrorOnRequestStream() { - $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); + $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf(\Exception::class)); $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -2029,7 +1980,7 @@ public function testRequestUnexpectedEndOfRequestWithChunkedTransferConnectionWi public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() { - $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); + $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf(\Exception::class)); $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -2051,7 +2002,7 @@ public function testRequestInvalidChunkHeaderWillEmitErrorOnRequestStream() public function testRequestUnexpectedEndOfRequestWithContentLengthWillEmitErrorOnRequestStream() { - $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); + $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf(\Exception::class)); $server = new StreamingServer(Loop::get(), function ($request) use ($errorEvent){ $request->getBody()->on('error', $errorEvent); }); @@ -2134,12 +2085,10 @@ public function testResponseWithBodyStreamWillUseChunkedTransferEncodingByDefaul $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2150,8 +2099,8 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); $stream->emit('data', ['hello']); - $this->assertContainsString("Transfer-Encoding: chunked", $buffer); - $this->assertContainsString("hello", $buffer); + $this->assertStringContainsString("Transfer-Encoding: chunked", $buffer); + $this->assertStringContainsString("hello", $buffer); } public function testResponseWithBodyStringWillOverwriteExplicitContentLengthAndTransferEncoding() @@ -2171,12 +2120,10 @@ public function testResponseWithBodyStringWillOverwriteExplicitContentLengthAndT $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2186,14 +2133,14 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertNotContainsString("Transfer-Encoding: chunked", $buffer); - $this->assertContainsString("Content-Length: 5", $buffer); - $this->assertContainsString("hello", $buffer); + $this->assertStringNotContainsString("Transfer-Encoding: chunked", $buffer); + $this->assertStringContainsString("Content-Length: 5", $buffer); + $this->assertStringContainsString("hello", $buffer); } public function testResponseContainsResponseBodyWithTransferEncodingChunkedForBodyWithUnknownSize() { - $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); + $body = $this->createMock(StreamInterface::class); $body->expects($this->once())->method('getSize')->willReturn(null); $body->expects($this->once())->method('__toString')->willReturn('body'); @@ -2209,12 +2156,10 @@ public function testResponseContainsResponseBodyWithTransferEncodingChunkedForBo $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2223,14 +2168,14 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("Transfer-Encoding: chunked", $buffer); - $this->assertNotContainsString("Content-Length:", $buffer); - $this->assertContainsString("body", $buffer); + $this->assertStringContainsString("Transfer-Encoding: chunked", $buffer); + $this->assertStringNotContainsString("Content-Length:", $buffer); + $this->assertStringContainsString("body", $buffer); } public function testResponseContainsResponseBodyWithPlainBodyWithUnknownSizeForLegacyHttp10() { - $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); + $body = $this->createMock(StreamInterface::class); $body->expects($this->once())->method('getSize')->willReturn(null); $body->expects($this->once())->method('__toString')->willReturn('body'); @@ -2246,12 +2191,10 @@ public function testResponseContainsResponseBodyWithPlainBodyWithUnknownSizeForL $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2260,9 +2203,9 @@ function ($data) use (&$buffer) { $data = "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n"; $this->connection->emit('data', [$data]); - $this->assertNotContainsString("Transfer-Encoding: chunked", $buffer); - $this->assertNotContainsString("Content-Length:", $buffer); - $this->assertContainsString("body", $buffer); + $this->assertStringNotContainsString("Transfer-Encoding: chunked", $buffer); + $this->assertStringNotContainsString("Content-Length:", $buffer); + $this->assertStringContainsString("body", $buffer); } public function testResponseWithCustomTransferEncodingWillBeIgnoredAndUseChunkedTransferEncodingInstead() @@ -2282,12 +2225,10 @@ public function testResponseWithCustomTransferEncodingWillBeIgnoredAndUseChunked $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2298,9 +2239,9 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); $stream->emit('data', ['hello']); - $this->assertContainsString('Transfer-Encoding: chunked', $buffer); - $this->assertNotContainsString('Transfer-Encoding: custom', $buffer); - $this->assertContainsString("5\r\nhello\r\n", $buffer); + $this->assertStringContainsString('Transfer-Encoding: chunked', $buffer); + $this->assertStringNotContainsString('Transfer-Encoding: custom', $buffer); + $this->assertStringContainsString("5\r\nhello\r\n", $buffer); } public function testResponseWithoutExplicitDateHeaderWillAddCurrentDateFromClock() @@ -2321,12 +2262,10 @@ public function testResponseWithoutExplicitDateHeaderWillAddCurrentDateFromClock $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2336,9 +2275,9 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("Date: Thu, 19 May 2022 14:54:51 GMT\r\n", $buffer); - $this->assertContainsString("\r\n\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("Date: Thu, 19 May 2022 14:54:51 GMT\r\n", $buffer); + $this->assertStringContainsString("\r\n\r\n", $buffer); } public function testResponseWithCustomDateHeaderOverwritesDefault() @@ -2354,12 +2293,10 @@ public function testResponseWithCustomDateHeaderOverwritesDefault() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2369,9 +2306,9 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", $buffer); - $this->assertContainsString("\r\n\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", $buffer); + $this->assertStringContainsString("\r\n\r\n", $buffer); } public function testResponseWithEmptyDateHeaderRemovesDateHeader() @@ -2387,12 +2324,10 @@ public function testResponseWithEmptyDateHeaderRemovesDateHeader() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2402,9 +2337,9 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertNotContainsString("Date:", $buffer); - $this->assertContainsString("\r\n\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringNotContainsString("Date:", $buffer); + $this->assertStringContainsString("\r\n\r\n", $buffer); } public function testResponseCanContainMultipleCookieHeaders() @@ -2427,12 +2362,10 @@ public function testResponseCanContainMultipleCookieHeaders() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2455,12 +2388,10 @@ public function testReponseWithExpectContinueRequestContainsContinueWithLaterRes $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2473,8 +2404,8 @@ function ($data) use (&$buffer) { $data .= "\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 100 Continue\r\n", $buffer); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 100 Continue\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); } public function testResponseWithExpectContinueRequestWontSendContinueForHttp10() @@ -2487,12 +2418,10 @@ public function testResponseWithExpectContinueRequestWontSendContinueForHttp10() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2503,13 +2432,13 @@ function ($data) use (&$buffer) { $data .= "\r\n"; $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.0 200 OK\r\n", $buffer); - $this->assertNotContainsString("HTTP/1.1 100 Continue\r\n\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.0 200 OK\r\n", $buffer); + $this->assertStringNotContainsString("HTTP/1.1 100 Continue\r\n\r\n", $buffer); } public function testInvalidCallbackFunctionLeadsToException() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new StreamingServer(Loop::get(), 'invalid'); } @@ -2529,12 +2458,10 @@ public function testResponseBodyStreamWillStreamDataWithChunkedTransferEncoding( $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2546,10 +2473,10 @@ function ($data) use (&$buffer) { $input->emit('data', ['1']); $input->emit('data', ['23']); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("\r\n\r\n", $buffer); - $this->assertContainsString("1\r\n1\r\n", $buffer); - $this->assertContainsString("2\r\n23\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("\r\n\r\n", $buffer); + $this->assertStringContainsString("1\r\n1\r\n", $buffer); + $this->assertStringContainsString("2\r\n23\r\n", $buffer); } public function testResponseBodyStreamWithContentLengthWillStreamTillLengthWithoutTransferEncoding() @@ -2568,12 +2495,10 @@ public function testResponseBodyStreamWithContentLengthWillStreamTillLengthWitho $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2585,11 +2510,11 @@ function ($data) use (&$buffer) { $input->emit('data', ['hel']); $input->emit('data', ['lo']); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("Content-Length: 5\r\n", $buffer); - $this->assertNotContainsString("Transfer-Encoding", $buffer); - $this->assertContainsString("\r\n\r\n", $buffer); - $this->assertContainsString("hello", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("Content-Length: 5\r\n", $buffer); + $this->assertStringNotContainsString("Transfer-Encoding", $buffer); + $this->assertStringContainsString("\r\n\r\n", $buffer); + $this->assertStringContainsString("hello", $buffer); } public function testResponseWithResponsePromise() @@ -2602,12 +2527,10 @@ public function testResponseWithResponsePromise() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2616,8 +2539,8 @@ function ($data) use (&$buffer) { $data = $this->createGetRequest(); $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("\r\n\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("\r\n\r\n", $buffer); } public function testResponseReturnInvalidTypeWillResultInError() @@ -2635,12 +2558,10 @@ public function testResponseReturnInvalidTypeWillResultInError() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2650,8 +2571,8 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertStringContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertInstanceOf(\RuntimeException::class, $exception); } public function testResponseResolveWrongTypeInPromiseWillResultInError() @@ -2664,12 +2585,10 @@ public function testResponseResolveWrongTypeInPromiseWillResultInError() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2679,7 +2598,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } public function testResponseRejectedPromiseWillResultInErrorMessage() @@ -2695,12 +2614,10 @@ public function testResponseRejectedPromiseWillResultInErrorMessage() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2710,7 +2627,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } public function testResponseExceptionInCallbackWillResultInErrorMessage() @@ -2726,12 +2643,10 @@ public function testResponseExceptionInCallbackWillResultInErrorMessage() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2741,7 +2656,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); } public function testResponseWithContentLengthHeaderForStringBodyOverwritesTransferEncoding() @@ -2758,12 +2673,10 @@ public function testResponseWithContentLengthHeaderForStringBodyOverwritesTransf $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2773,11 +2686,11 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); - $this->assertContainsString("Content-Length: 5\r\n", $buffer); - $this->assertContainsString("hello", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("Content-Length: 5\r\n", $buffer); + $this->assertStringContainsString("hello", $buffer); - $this->assertNotContainsString("Transfer-Encoding", $buffer); + $this->assertStringNotContainsString("Transfer-Encoding", $buffer); } public function testResponseWillBeHandled() @@ -2790,12 +2703,10 @@ public function testResponseWillBeHandled() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2805,7 +2716,7 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 200 OK\r\n", $buffer); + $this->assertStringContainsString("HTTP/1.1 200 OK\r\n", $buffer); } public function testResponseExceptionThrowInCallBackFunctionWillResultInErrorMessage() @@ -2823,12 +2734,10 @@ public function testResponseExceptionThrowInCallBackFunctionWillResultInErrorMes $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2838,8 +2747,8 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertInstanceOf('RuntimeException', $exception); - $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertInstanceOf(\RuntimeException::class, $exception); + $this->assertStringContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); $this->assertEquals('hello', $exception->getPrevious()->getMessage()); } @@ -2858,12 +2767,10 @@ public function testResponseThrowableThrowInCallBackFunctionWillResultInErrorMes $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2881,8 +2788,8 @@ function ($data) use (&$buffer) { ); } - $this->assertInstanceOf('RuntimeException', $exception); - $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertInstanceOf(\RuntimeException::class, $exception); + $this->assertStringContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); $this->assertEquals('hello', $exception->getPrevious()->getMessage()); } @@ -2903,12 +2810,10 @@ public function testResponseRejectOfNonExceptionWillResultInErrorMessage() $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -2918,54 +2823,52 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertStringContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertInstanceOf(\RuntimeException::class, $exception); } public static function provideInvalidResponse() { $response = new Response(200, [], '', '1.1', 'OK'); - return [ - [ - $response->withStatus(99, 'OK') - ], - [ - $response->withStatus(1000, 'OK') - ], - [ - $response->withStatus(200, "Invald\r\nReason: Yes") - ], - [ - $response->withHeader('Invalid', "Yes\r\n") - ], - [ - $response->withHeader('Invalid', "Yes\n") - ], - [ - $response->withHeader('Invalid', "Yes\r") - ], - [ - $response->withHeader("Inva\r\nlid", 'Yes') - ], - [ - $response->withHeader("Inva\nlid", 'Yes') - ], - [ - $response->withHeader("Inva\rlid", 'Yes') - ], - [ - $response->withHeader('Inva Lid', 'Yes') - ], - [ - $response->withHeader('Inva:Lid', 'Yes') - ], - [ - $response->withHeader('Invalid', "Val\0ue") - ], - [ - $response->withHeader("Inva\0lid", 'Yes') - ] + yield [ + $response->withStatus(99, 'OK') + ]; + yield [ + $response->withStatus(1000, 'OK') + ]; + yield [ + $response->withStatus(200, "Invald\r\nReason: Yes") + ]; + yield [ + $response->withHeader('Invalid', "Yes\r\n") + ]; + yield [ + $response->withHeader('Invalid', "Yes\n") + ]; + yield [ + $response->withHeader('Invalid', "Yes\r") + ]; + yield [ + $response->withHeader("Inva\r\nlid", 'Yes') + ]; + yield [ + $response->withHeader("Inva\nlid", 'Yes') + ]; + yield [ + $response->withHeader("Inva\rlid", 'Yes') + ]; + yield [ + $response->withHeader('Inva Lid', 'Yes') + ]; + yield [ + $response->withHeader('Inva:Lid', 'Yes') + ]; + yield [ + $response->withHeader('Invalid', "Val\0ue") + ]; + yield [ + $response->withHeader("Inva\0lid", 'Yes') ]; } @@ -2988,12 +2891,10 @@ public function testInvalidResponseObjectWillResultInErrorMessage(ResponseInterf $this->connection ->expects($this->any()) ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) + ->willReturnCallback( + function ($data) use (&$buffer) { + $buffer .= $data; + } ); $server->listen($this->socket); @@ -3003,8 +2904,8 @@ function ($data) use (&$buffer) { $this->connection->emit('data', [$data]); - $this->assertContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); - $this->assertInstanceOf('InvalidArgumentException', $exception); + $this->assertStringContainsString("HTTP/1.1 500 Internal Server Error\r\n", $buffer); + $this->assertInstanceOf(\InvalidArgumentException::class, $exception); } public function testRequestServerRequestParams() @@ -3147,7 +3048,7 @@ public function testNewConnectionWillInvokeParserOnce() { $server = new StreamingServer(Loop::get(), $this->expectCallableNever()); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); + $parser = $this->createMock(RequestHeaderParser::class); $parser->expects($this->once())->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3164,7 +3065,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen $server = new StreamingServer(Loop::get(), $this->expectCallableOnceWith($request)); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); + $parser = $this->createMock(RequestHeaderParser::class); $parser->expects($this->once())->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3187,7 +3088,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen $server = new StreamingServer(Loop::get(), $this->expectCallableOnceWith($request)); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); + $parser = $this->createMock(RequestHeaderParser::class); $parser->expects($this->once())->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3212,7 +3113,7 @@ public function testNewConnectionWillInvokeParserOnceAndInvokeRequestHandlerWhen return new Response(200, ['Connection' => 'close']); }); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); + $parser = $this->createMock(RequestHeaderParser::class); $parser->expects($this->once())->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3237,7 +3138,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle return new Response(); }); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); + $parser = $this->createMock(RequestHeaderParser::class); $parser->expects($this->exactly(2))->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3262,7 +3163,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle return new Response(); }); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); + $parser = $this->createMock(RequestHeaderParser::class); $parser->expects($this->exactly(2))->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3288,7 +3189,7 @@ public function testNewConnectionWillInvokeParserOnceAfterInvokingRequestHandler return new Response(200, [], $body); }); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); + $parser = $this->createMock(RequestHeaderParser::class); $parser->expects($this->once())->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); @@ -3314,7 +3215,7 @@ public function testNewConnectionWillInvokeParserTwiceAfterInvokingRequestHandle return new Response(200, [], $body); }); - $parser = $this->getMockBuilder('React\Http\Io\RequestHeaderParser')->disableOriginalConstructor()->getMock(); + $parser = $this->createMock(RequestHeaderParser::class); $parser->expects($this->exactly(2))->method('handle'); $ref = new \ReflectionProperty($server, 'parser'); diff --git a/tests/Io/TransactionTest.php b/tests/Io/TransactionTest.php index b4825024..3833cfd3 100644 --- a/tests/Io/TransactionTest.php +++ b/tests/Io/TransactionTest.php @@ -5,7 +5,10 @@ use PHPUnit\Framework\MockObject\MockObject; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use React\EventLoop\LoopInterface; +use React\EventLoop\TimerInterface; use React\Http\Io\ReadableBodyStream; +use React\Http\Io\Sender; use React\Http\Io\Transaction; use React\Http\Message\Request; use React\Http\Message\Response; @@ -13,6 +16,8 @@ use React\EventLoop\Loop; use React\Promise\Deferred; use React\Promise\Promise; +use React\Promise\PromiseInterface; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; use function React\Async\await; @@ -24,12 +29,12 @@ class TransactionTest extends TestCase public function testWithOptionsReturnsNewInstanceWithChangedOption() { $sender = $this->makeSenderMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $transaction = new Transaction($sender, $loop); $new = $transaction->withOptions(['followRedirects' => false]); - $this->assertInstanceOf('React\Http\Io\Transaction', $new); + $this->assertInstanceOf(Transaction::class, $new); $this->assertNotSame($transaction, $new); $ref = new \ReflectionProperty($new, 'followRedirects'); @@ -41,7 +46,7 @@ public function testWithOptionsReturnsNewInstanceWithChangedOption() public function testWithOptionsDoesNotChangeOriginalInstance() { $sender = $this->makeSenderMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $transaction = new Transaction($sender, $loop); $transaction->withOptions(['followRedirects' => false]); @@ -55,7 +60,7 @@ public function testWithOptionsDoesNotChangeOriginalInstance() public function testWithOptionsNullValueReturnsNewInstanceWithDefaultOption() { $sender = $this->makeSenderMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['followRedirects' => false]); @@ -69,34 +74,34 @@ public function testWithOptionsNullValueReturnsNewInstanceWithDefaultOption() public function testTimeoutExplicitOptionWillStartTimeoutTimer() { - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); } public function testTimeoutImplicitFromIniWillStartTimeoutTimer() { - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); @@ -105,24 +110,24 @@ public function testTimeoutImplicitFromIniWillStartTimeoutTimer() $promise = $transaction->send($request); ini_set('default_socket_timeout', $old); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); } public function testTimeoutExplicitOptionWillRejectWhenTimerFires() { $timeout = null; - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->with(2, $this->callback(function ($cb) use (&$timeout) { $timeout = $cb; return true; }))->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 2]); @@ -136,42 +141,42 @@ public function testTimeoutExplicitOptionWillRejectWhenTimerFires() $exception = $e; }); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); $this->assertEquals('Request timed out after 2 seconds', $exception->getMessage()); } public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderResolvesImmediately() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->never())->method('addTimer'); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $response = new Response(200, [], ''); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(resolve($response)); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(resolve($response)); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 0.001]); $promise = $transaction->send($request); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); $promise->then($this->expectCallableOnceWith($response)); } public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderResolvesLaterOn() { - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $response = new Response(200, [], ''); $deferred = new Deferred(); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn($deferred->promise()); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 0.001]); @@ -179,41 +184,41 @@ public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderResolve $deferred->resolve($response); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); $promise->then($this->expectCallableOnceWith($response)); } public function testTimeoutExplicitOptionWillNotStartTimeoutWhenSenderRejectsImmediately() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->never())->method('addTimer'); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $exception = new \RuntimeException(); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(reject($exception)); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(reject($exception)); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 0.001]); $promise = $transaction->send($request); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); $promise->then(null, $this->expectCallableOnceWith($exception)); } public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderRejectsLaterOn() { - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $deferred = new Deferred(); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn($deferred->promise()); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 0.001]); @@ -222,49 +227,49 @@ public function testTimeoutExplicitOptionWillCancelTimeoutTimerWhenSenderRejects $exception = new \RuntimeException(); $deferred->reject($exception); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); $promise->then(null, $this->expectCallableOnceWith($exception)); } public function testTimeoutExplicitNegativeWillNotStartTimeoutTimer() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->never())->method('addTimer'); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => -1]); $promise = $transaction->send($request); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); } public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenRequestBodyIsStreaming() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->never())->method('addTimer'); $stream = new ThroughStream(); $request = new Request('POST', 'http://example.com/', [], new ReadableBodyStream($stream)); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); } public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingRequestBodyIsAlreadyClosed() { - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); @@ -272,28 +277,28 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingReque $stream->close(); $request = new Request('POST', 'http://example.com/', [], new ReadableBodyStream($stream)); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 2]); $promise = $transaction->send($request); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); } public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingRequestBodyClosesWhileSenderIsStillPending() { - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->with(2, $this->anything())->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); $stream = new ThroughStream(); $request = new Request('POST', 'http://example.com/', [], new ReadableBodyStream($stream)); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 2]); @@ -301,20 +306,20 @@ public function testTimeoutExplicitOptionWillStartTimeoutTimerWhenStreamingReque $stream->close(); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); } public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenStreamingRequestBodyClosesAfterSenderRejects() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->never())->method('addTimer'); $stream = new ThroughStream(); $request = new Request('POST', 'http://example.com/', [], new ReadableBodyStream($stream)); $deferred = new Deferred(); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn($deferred->promise()); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn($deferred->promise()); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 2]); @@ -323,7 +328,7 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenStreamingRe $deferred->reject(new \RuntimeException('Request failed')); $stream->close(); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection } @@ -331,8 +336,8 @@ public function testTimeoutExplicitOptionWillNotStartTimeoutTimerWhenStreamingRe public function testTimeoutExplicitOptionWillRejectWhenTimerFiresAfterStreamingRequestBodyCloses() { $timeout = null; - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->with(2, $this->callback(function ($cb) use (&$timeout) { $timeout = $cb; return true; @@ -342,8 +347,8 @@ public function testTimeoutExplicitOptionWillRejectWhenTimerFiresAfterStreamingR $stream = new ThroughStream(); $request = new Request('POST', 'http://example.com/', [], new ReadableBodyStream($stream)); - $sender = $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); - $sender->expects($this->once())->method('send')->with($this->equalTo($request))->willReturn(new Promise(function () { })); + $sender = $this->createMock(Sender::class); + $sender->expects($this->once())->method('send')->with($request)->willReturn(new Promise(function () { })); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => 2]); @@ -359,19 +364,19 @@ public function testTimeoutExplicitOptionWillRejectWhenTimerFiresAfterStreamingR $exception = $e; }); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); $this->assertEquals('Request timed out after 2 seconds', $exception->getMessage()); } public function testReceivingErrorResponseWillRejectWithResponseException() { - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $response = new Response(404); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); // 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(resolve($response)); + $sender->expects($this->once())->method('send')->with($request)->willReturn(resolve($response)); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['timeout' => -1]); @@ -395,12 +400,12 @@ public function testReceivingStreamingBodyWillResolveWithBufferedResponseByDefau $stream->close(); }); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $response = new Response(200, [], 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(resolve($response)); + $sender->expects($this->once())->method('send')->with($request)->willReturn(resolve($response)); $transaction = new Transaction($sender, Loop::get()); $promise = $transaction->send($request); @@ -416,13 +421,13 @@ public function testReceivingStreamingBodyWithContentLengthExceedingMaximumRespo $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $response = new Response(200, ['Content-Length' => '100000000'], new ReadableBodyStream($stream, 100000000)); // 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(resolve($response)); + $sender->expects($this->once())->method('send')->with($request)->willReturn(resolve($response)); $transaction = new Transaction($sender, Loop::get()); @@ -436,7 +441,7 @@ public function testReceivingStreamingBodyWithContentLengthExceedingMaximumRespo $this->assertFalse($stream->isWritable()); assert($exception instanceof \OverflowException); - $this->assertInstanceOf('OverflowException', $exception); + $this->assertInstanceOf(\OverflowException::class, $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()); @@ -447,13 +452,13 @@ public function testReceivingStreamingBodyWithContentsExceedingMaximumResponseBu $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $response = new Response(200, [], 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(resolve($response)); + $sender->expects($this->once())->method('send')->with($request)->willReturn(resolve($response)); $transaction = new Transaction($sender, Loop::get()); $transaction = $transaction->withOptions(['maximumSize' => 10]); @@ -469,7 +474,7 @@ public function testReceivingStreamingBodyWithContentsExceedingMaximumResponseBu $this->assertFalse($stream->isWritable()); assert($exception instanceof \OverflowException); - $this->assertInstanceOf('OverflowException', $exception); + $this->assertInstanceOf(\OverflowException::class, $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()); @@ -481,12 +486,12 @@ public function testReceivingStreamingBodyWillRejectWhenStreamEmitsError() throw new \UnexpectedValueException('Unexpected ' . $data, 42); }); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $response = new Response(200, [], 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(resolve($response)); + $sender->expects($this->once())->method('send')->with($request)->willReturn(resolve($response)); $transaction = new Transaction($sender, Loop::get()); $promise = $transaction->send($request); @@ -501,25 +506,25 @@ public function testReceivingStreamingBodyWillRejectWhenStreamEmitsError() $this->assertFalse($stream->isWritable()); assert($exception instanceof \RuntimeException); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); $this->assertEquals('Error while buffering response body: Unexpected Foo', $exception->getMessage()); $this->assertEquals(42, $exception->getCode()); - $this->assertInstanceOf('UnexpectedValueException', $exception->getPrevious()); + $this->assertInstanceOf(\UnexpectedValueException::class, $exception->getPrevious()); } public function testCancelBufferingResponseWillCloseStreamAndReject() { - $stream = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $stream = $this->createMock(ReadableStreamInterface::class); $stream->expects($this->any())->method('isReadable')->willReturn(true); $stream->expects($this->once())->method('close'); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); + $request = $this->createMock(RequestInterface::class); $response = new Response(200, [], 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($deferred->promise()); + $sender->expects($this->once())->method('send')->with($request)->willReturn($deferred->promise()); $transaction = new Transaction($sender, Loop::get()); $promise = $transaction->send($request); @@ -533,7 +538,7 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() }); assert($exception instanceof \RuntimeException); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); $this->assertEquals('Cancelled buffering response body', $exception->getMessage()); $this->assertEquals(0, $exception->getCode()); $this->assertNull($exception->getPrevious()); @@ -541,14 +546,14 @@ public function testCancelBufferingResponseWillCloseStreamAndReject() public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStreamingIsEnabled() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); - $request = $this->getMockBuilder('Psr\Http\Message\RequestInterface')->getMock(); - $response = new Response(200, [], new ReadableBodyStream($this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock())); + $request = $this->createMock(RequestInterface::class); + $response = new Response(200, [], new ReadableBodyStream($this->createMock(ReadableStreamInterface::class))); // 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(resolve($response)); + $sender->expects($this->once())->method('send')->with($request)->willReturn(resolve($response)); $transaction = new Transaction($sender, $loop); $transaction = $transaction->withOptions(['streaming' => true, 'timeout' => -1]); @@ -566,7 +571,7 @@ public function testReceivingStreamingBodyWillResolveWithStreamingResponseIfStre public function testResponseCode304WithoutLocationWillResolveWithResponseAsIs() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); // conditional GET request will respond with 304 (Not Modified $request = new Request('GET', 'http://example.com/', ['If-None-Match' => '"abc"']); @@ -583,7 +588,7 @@ public function testResponseCode304WithoutLocationWillResolveWithResponseAsIs() public function testCustomRedirectResponseCode333WillFollowLocationHeaderAndSendRedirectedRequest() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); // original GET request will respond with custom 333 redirect status code and follow location header $requestOriginal = new Request('GET', 'http://example.com/'); @@ -605,7 +610,7 @@ public function testCustomRedirectResponseCode333WillFollowLocationHeaderAndSend public function testFollowingRedirectWithSpecifiedHeaders() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $customHeaders = ['User-Agent' => 'Chrome']; $requestWithUserAgent = new Request('GET', 'http://example.com/', $customHeaders); @@ -635,7 +640,7 @@ public function testFollowingRedirectWithSpecifiedHeaders() public function testRemovingAuthorizationHeaderWhenChangingHostnamesDuringRedirect() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $customHeaders = ['Authorization' => 'secret']; $requestWithAuthorization = new Request('GET', 'http://example.com/', $customHeaders); @@ -665,7 +670,7 @@ public function testRemovingAuthorizationHeaderWhenChangingHostnamesDuringRedire public function testAuthorizationHeaderIsForwardedWhenRedirectingToSameDomain() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $customHeaders = ['Authorization' => 'secret']; $requestWithAuthorization = new Request('GET', 'http://example.com/', $customHeaders); @@ -695,7 +700,7 @@ public function testAuthorizationHeaderIsForwardedWhenRedirectingToSameDomain() public function testAuthorizationHeaderIsForwardedWhenLocationContainsAuthentication() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $request = new Request('GET', 'http://example.com/'); $sender = $this->makeSenderMock(); @@ -725,7 +730,7 @@ public function testAuthorizationHeaderIsForwardedWhenLocationContainsAuthentica public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $customHeaders = [ 'Content-Type' => 'text/html; charset=utf-8', @@ -760,7 +765,7 @@ public function testSomeRequestHeadersShouldBeRemovedWhenRedirecting() public function testRequestMethodShouldBeChangedWhenRedirectingWithSeeOther() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $customHeaders = [ 'Content-Type' => 'text/html; charset=utf-8', @@ -796,7 +801,7 @@ public function testRequestMethodShouldBeChangedWhenRedirectingWithSeeOther() public function testRequestMethodAndBodyShouldNotBeChangedWhenRedirectingWith307Or308() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $customHeaders = [ 'Content-Type' => 'text/html; charset=utf-8', @@ -839,7 +844,7 @@ public function testRequestMethodAndBodyShouldNotBeChangedWhenRedirectingWith307 public function testRedirectingStreamingBodyWith307Or308ShouldThrowCantRedirectStreamException() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $customHeaders = [ 'Content-Type' => 'text/html; charset=utf-8', @@ -874,7 +879,7 @@ public function testRedirectingStreamingBodyWith307Or308ShouldThrowCantRedirectS public function testCancelTransactionWillCancelRequest() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $request = new Request('GET', 'http://example.com/'); $sender = $this->makeSenderMock(); @@ -892,8 +897,8 @@ public function testCancelTransactionWillCancelRequest() public function testCancelTransactionWillCancelTimeoutTimer() { - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); + $loop = $this->createMock(LoopInterface::class); $loop->expects($this->once())->method('addTimer')->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); @@ -914,7 +919,7 @@ public function testCancelTransactionWillCancelTimeoutTimer() public function testCancelTransactionWillCancelRedirectedRequest() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $request = new Request('GET', 'http://example.com/'); $sender = $this->makeSenderMock(); @@ -941,7 +946,7 @@ public function testCancelTransactionWillCancelRedirectedRequest() public function testCancelTransactionWillCancelRedirectedRequestAgain() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $request = new Request('GET', 'http://example.com/'); $sender = $this->makeSenderMock(); @@ -971,7 +976,7 @@ public function testCancelTransactionWillCancelRedirectedRequestAgain() public function testCancelTransactionWillCloseBufferingStream() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $request = new Request('GET', 'http://example.com/'); $sender = $this->makeSenderMock(); @@ -994,7 +999,7 @@ public function testCancelTransactionWillCloseBufferingStream() public function testCancelTransactionWillCloseBufferingStreamAgain() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $request = new Request('GET', 'http://example.com/'); $sender = $this->makeSenderMock(); @@ -1015,7 +1020,7 @@ public function testCancelTransactionWillCloseBufferingStreamAgain() public function testCancelTransactionShouldCancelSendingPromise() { - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop = $this->createMock(LoopInterface::class); $request = new Request('GET', 'http://example.com/'); $sender = $this->makeSenderMock(); @@ -1045,6 +1050,6 @@ public function testCancelTransactionShouldCancelSendingPromise() */ private function makeSenderMock() { - return $this->getMockBuilder('React\Http\Io\Sender')->disableOriginalConstructor()->getMock(); + return $this->createMock(Sender::class); } } diff --git a/tests/Io/UploadedFileTest.php b/tests/Io/UploadedFileTest.php index adbed51c..529b75af 100644 --- a/tests/Io/UploadedFileTest.php +++ b/tests/Io/UploadedFileTest.php @@ -8,14 +8,12 @@ class UploadedFileTest extends TestCase { - public function failtyErrorProvider() + public static function failtyErrorProvider() { - return [ - ['a'], - [null], - [-1], - [9] - ]; + yield ['a']; + yield [null]; + yield [-1]; + yield [9]; } /** @@ -25,7 +23,8 @@ public function testFailtyError($error) { $stream = new BufferedBody(''); - $this->setExpectedException('InvalidArgumentException', 'Invalid error code, must be an UPLOAD_ERR_* constant'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid error code, must be an UPLOAD_ERR_* constant'); new UploadedFile($stream, 0, $error, 'foo.bar', 'foo/bar'); } @@ -34,7 +33,8 @@ public function testNoMoveFile() $stream = new BufferedBody(''); $uploadedFile = new UploadedFile($stream, 0, UPLOAD_ERR_OK, 'foo.bar', 'foo/bar'); - $this->setExpectedException('RuntimeException', 'Not implemented'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Not implemented'); $uploadedFile->moveTo('bar.foo'); } @@ -54,7 +54,8 @@ public function testGetStreamOnFailedUpload() $stream = new BufferedBody(''); $uploadedFile = new UploadedFile($stream, 0, UPLOAD_ERR_NO_FILE, 'foo.bar', 'foo/bar'); - $this->setExpectedException('RuntimeException', 'Cannot retrieve stream due to upload error'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Cannot retrieve stream due to upload error'); $uploadedFile->getStream(); } } diff --git a/tests/Message/RequestTest.php b/tests/Message/RequestTest.php index 543ddb88..148536a0 100644 --- a/tests/Message/RequestTest.php +++ b/tests/Message/RequestTest.php @@ -2,8 +2,10 @@ namespace React\Tests\Http\Message; +use Psr\Http\Message\StreamInterface; use React\Http\Io\HttpBodyStream; use React\Http\Message\Request; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; @@ -33,8 +35,8 @@ public function testConstructWithStreamingRequestBodyReturnsBodyWhichImplementsR ); $body = $request->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(StreamInterface::class, $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); $this->assertNull($body->getSize()); } @@ -52,7 +54,8 @@ public function testConstructWithHttpBodyStreamReturnsBodyAsIs() public function testConstructWithNullBodyThrows() { - $this->setExpectedException('InvalidArgumentException', 'Invalid request body given'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid request body given'); new Request( 'GET', 'http://localhost/', diff --git a/tests/Message/ResponseTest.php b/tests/Message/ResponseTest.php index 61acf19e..6d4acb73 100644 --- a/tests/Message/ResponseTest.php +++ b/tests/Message/ResponseTest.php @@ -2,8 +2,10 @@ namespace React\Tests\Http\Message; +use Psr\Http\Message\StreamInterface; use React\Http\Io\HttpBodyStream; use React\Http\Message\Response; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; @@ -15,7 +17,7 @@ public function testConstructWithStringBodyWillReturnStreamInstance() $body = $response->getBody(); /** @var \Psr\Http\Message\StreamInterface $body */ - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); + $this->assertInstanceOf(StreamInterface::class, $body); $this->assertEquals('hello', (string) $body); } @@ -25,9 +27,9 @@ public function testConstructWithStreamingBodyWillReturnReadableBodyStream() $body = $response->getBody(); /** @var \Psr\Http\Message\StreamInterface $body */ - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceof('React\Stream\ReadableStreamInterface', $body); - $this->assertInstanceOf('React\Http\Io\HttpBodyStream', $body); + $this->assertInstanceOf(StreamInterface::class, $body); + $this->assertInstanceof(ReadableStreamInterface::class, $body); + $this->assertInstanceOf(HttpBodyStream::class, $body); $this->assertNull($body->getSize()); } @@ -44,13 +46,13 @@ public function testConstructWithHttpBodyStreamReturnsBodyAsIs() public function testFloatBodyWillThrow() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new Response(200, [], 1.0); } public function testResourceBodyWillThrow() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new Response(200, [], tmpfile()); } @@ -126,7 +128,8 @@ public function testJsonMethodReturnsJsonTextForSimpleString() public function testJsonMethodThrowsForInvalidString() { - $this->setExpectedException('InvalidArgumentException', 'Unable to encode given data as JSON: Malformed UTF-8 characters, possibly incorrectly encoded'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to encode given data as JSON: Malformed UTF-8 characters, possibly incorrectly encoded'); Response::json("Hello w\xF6rld!"); } @@ -220,25 +223,25 @@ public function testParseMessageWithHttp10SimpleOkResponseWithLegacyNewlines() public function testParseMessageWithInvalidHttpProtocolVersion12Throws() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); Response::parseMessage("HTTP/1.2 200 OK\r\n"); } public function testParseMessageWithInvalidHttpProtocolVersion2Throws() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); Response::parseMessage("HTTP/2 200 OK\r\n"); } public function testParseMessageWithInvalidStatusCodeUnderflowThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); Response::parseMessage("HTTP/1.1 99 OK\r\n"); } public function testParseMessageWithInvalidResponseHeaderFieldThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); Response::parseMessage("HTTP/1.1 200 OK\r\nServer\r\n"); } } diff --git a/tests/Message/ServerRequestTest.php b/tests/Message/ServerRequestTest.php index 36d20bfa..596ebf47 100644 --- a/tests/Message/ServerRequestTest.php +++ b/tests/Message/ServerRequestTest.php @@ -2,8 +2,10 @@ namespace React\Tests\Http\Message; +use Psr\Http\Message\StreamInterface; use React\Http\Io\HttpBodyStream; use React\Http\Message\ServerRequest; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; @@ -302,8 +304,8 @@ public function testConstructWithStreamingRequestBodyReturnsBodyWhichImplementsR ); $body = $request->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(StreamInterface::class, $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); $this->assertSame(0, $body->getSize()); } @@ -319,8 +321,8 @@ public function testConstructWithStreamingRequestBodyReturnsBodyWithSizeFromCont ); $body = $request->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(StreamInterface::class, $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); $this->assertSame(100, $body->getSize()); } @@ -336,14 +338,14 @@ public function testConstructWithStreamingRequestBodyReturnsBodyWithSizeUnknownF ); $body = $request->getBody(); - $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $body); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + $this->assertInstanceOf(StreamInterface::class, $body); + $this->assertInstanceOf(ReadableStreamInterface::class, $body); $this->assertNull($body->getSize()); } public function testConstructWithFloatRequestBodyThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new ServerRequest( 'GET', 'http://localhost/', @@ -354,7 +356,7 @@ public function testConstructWithFloatRequestBodyThrows() public function testConstructWithResourceRequestBodyThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new ServerRequest( 'GET', 'http://localhost/', @@ -403,85 +405,85 @@ public function testParseMessageWithConnectMethodWithAuthorityFormRequestTarget( public function testParseMessageWithInvalidHttp11RequestWithoutHostThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.1\r\n", []); } public function testParseMessageWithInvalidHttpProtocolVersionThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.2\r\n", []); } public function testParseMessageWithInvalidProtocolThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / CUSTOM/1.1\r\n", []); } public function testParseMessageWithInvalidHostHeaderWithoutValueThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost\r\n", []); } public function testParseMessageWithInvalidHostHeaderSyntaxThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: ///\r\n", []); } public function testParseMessageWithInvalidHostHeaderWithSchemeThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: http://localhost\r\n", []); } public function testParseMessageWithInvalidHostHeaderWithQueryThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost?foo\r\n", []); } public function testParseMessageWithInvalidHostHeaderWithFragmentThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost#foo\r\n", []); } public function testParseMessageWithInvalidContentLengthHeaderThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length:\r\n", []); } public function testParseMessageWithInvalidTransferEncodingHeaderThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding:\r\n", []); } public function testParseMessageWithInvalidBothContentLengthHeaderAndTransferEncodingHeaderThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\nTransfer-Encoding: chunked\r\n", []); } public function testParseMessageWithInvalidEmptyHostHeaderWithAbsoluteFormRequestTargetThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET http://example.com/ HTTP/1.1\r\nHost: \r\n", []); } public function testParseMessageWithInvalidConnectMethodNotUsingAuthorityFormThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("CONNECT / HTTP/1.1\r\nHost: localhost\r\n", []); } public function testParseMessageWithInvalidRequestTargetAsteriskFormThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); ServerRequest::parseMessage("GET * HTTP/1.1\r\nHost: localhost\r\n", []); } } diff --git a/tests/Message/UriTest.php b/tests/Message/UriTest.php index cdbc5a87..adaee94b 100644 --- a/tests/Message/UriTest.php +++ b/tests/Message/UriTest.php @@ -9,118 +9,116 @@ class UriTest extends TestCase { public function testCtorWithInvalidSyntaxThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new Uri('///'); } public function testCtorWithInvalidSchemeThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new Uri('not+a+scheme://localhost'); } public function testCtorWithInvalidHostThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new Uri('http://not a host/'); } public function testCtorWithInvalidPortThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new Uri('http://localhost:80000/'); } public static function provideValidUris() { - return [ - [ - 'http://localhost/' - ], - [ - 'http://localhost/' - ], - [ - 'http://localhost:8080/' - ], - [ - 'http://127.0.0.1/' - ], - [ - 'http://[::1]:8080/' - ], - [ - 'http://localhost/path' - ], - [ - 'http://localhost/sub/path' - ], - [ - 'http://localhost/with%20space' - ], - [ - 'http://localhost/with%2fslash' - ], - [ - 'http://localhost/?name=Alice' - ], - [ - 'http://localhost/?name=John+Doe' - ], - [ - 'http://localhost/?name=John%20Doe' - ], - [ - 'http://localhost/?name=Alice&age=42' - ], - [ - 'http://localhost/?name=Alice&' - ], - [ - 'http://localhost/?choice=A%26B' - ], - [ - 'http://localhost/?safe=Yes!?' - ], - [ - 'http://localhost/?alias=@home' - ], - [ - 'http://localhost/?assign:=true' - ], - [ - 'http://localhost/?name=' - ], - [ - 'http://localhost/?name' - ], - [ - '' - ], - [ - '/' - ], - [ - '/path' - ], - [ - 'path' - ], - [ - 'http://user@localhost/' - ], - [ - 'http://user@localhost/' - ], - [ - 'http://:pass@localhost/' - ], - [ - 'http://user:pass@localhost/path?query#fragment' - ], - [ - 'http://user%20name:pass%20word@localhost/path%20name?query%20name#frag%20ment' - ] + yield [ + 'http://localhost/' + ]; + yield [ + 'http://localhost/' + ]; + yield [ + 'http://localhost:8080/' + ]; + yield [ + 'http://127.0.0.1/' + ]; + yield [ + 'http://[::1]:8080/' + ]; + yield [ + 'http://localhost/path' + ]; + yield [ + 'http://localhost/sub/path' + ]; + yield [ + 'http://localhost/with%20space' + ]; + yield [ + 'http://localhost/with%2fslash' + ]; + yield [ + 'http://localhost/?name=Alice' + ]; + yield [ + 'http://localhost/?name=John+Doe' + ]; + yield [ + 'http://localhost/?name=John%20Doe' + ]; + yield [ + 'http://localhost/?name=Alice&age=42' + ]; + yield [ + 'http://localhost/?name=Alice&' + ]; + yield [ + 'http://localhost/?choice=A%26B' + ]; + yield [ + 'http://localhost/?safe=Yes!?' + ]; + yield [ + 'http://localhost/?alias=@home' + ]; + yield [ + 'http://localhost/?assign:=true' + ]; + yield [ + 'http://localhost/?name=' + ]; + yield [ + 'http://localhost/?name' + ]; + yield [ + '' + ]; + yield [ + '/' + ]; + yield [ + '/path' + ]; + yield [ + 'path' + ]; + yield [ + 'http://user@localhost/' + ]; + yield [ + 'http://user@localhost/' + ]; + yield [ + 'http://:pass@localhost/' + ]; + yield [ + 'http://user:pass@localhost/path?query#fragment' + ]; + yield [ + 'http://user%20name:pass%20word@localhost/path%20name?query%20name#frag%20ment' ]; } @@ -137,35 +135,33 @@ public function testToStringReturnsOriginalUriGivenToCtor($string) public static function provideValidUrisThatWillBeTransformed() { - return [ - [ - 'http://localhost:8080/?', - 'http://localhost:8080/' - ], - [ - 'http://localhost:8080/#', - 'http://localhost:8080/' - ], - [ - 'http://localhost:8080/?#', - 'http://localhost:8080/' - ], - [ - 'http://localhost:8080/', - 'http://localhost:8080/' - ], - [ - 'http://localhost:8080/?percent=50%', - 'http://localhost:8080/?percent=50%25' - ], - [ - 'http://user%20name:pass%20word@localhost/path%20name?query%20name#frag%20ment', - 'http://user%20name:pass%20word@localhost/path%20name?query%20name#frag%20ment' - ], - [ - 'HTTP://USER:PASS@LOCALHOST:8080/PATH?QUERY#FRAGMENT', - 'http://USER:PASS@localhost:8080/PATH?QUERY#FRAGMENT' - ] + yield [ + 'http://localhost:8080/?', + 'http://localhost:8080/' + ]; + yield [ + 'http://localhost:8080/#', + 'http://localhost:8080/' + ]; + yield [ + 'http://localhost:8080/?#', + 'http://localhost:8080/' + ]; + yield [ + 'http://localhost:8080/', + 'http://localhost:8080/' + ]; + yield [ + 'http://localhost:8080/?percent=50%', + 'http://localhost:8080/?percent=50%25' + ]; + yield [ + 'http://user%20name:pass%20word@localhost/path%20name?query%20name#frag%20ment', + 'http://user%20name:pass%20word@localhost/path%20name?query%20name#frag%20ment' + ]; + yield [ + 'HTTP://USER:PASS@LOCALHOST:8080/PATH?QUERY#FRAGMENT', + 'http://USER:PASS@localhost:8080/PATH?QUERY#FRAGMENT' ]; } @@ -251,7 +247,7 @@ public function testWithSchemeThrowsWhenSchemeIsInvalid() { $uri = new Uri('http://localhost/'); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $uri->withScheme('invalid+scheme'); } @@ -375,7 +371,7 @@ public function testWithHostThrowsWhenHostIsInvalidWithPlus() { $uri = new Uri('http://localhost/'); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $uri->withHost('invalid+host'); } @@ -383,7 +379,7 @@ public function testWithHostThrowsWhenHostIsInvalidWithSpace() { $uri = new Uri('http://localhost/'); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $uri->withHost('invalid host'); } @@ -448,7 +444,7 @@ public function testWithPortThrowsWhenPortIsInvalidUnderflow() { $uri = new Uri('http://localhost/'); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $uri->withPort(0); } @@ -456,7 +452,7 @@ public function testWithPortThrowsWhenPortIsInvalidOverflow() { $uri = new Uri('http://localhost/'); - $this->setExpectedException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $uri->withPort(65536); } @@ -576,112 +572,110 @@ public function testWithFragmentReturnsSameInstanceWhenFragmentIsUnchangedEncode public static function provideResolveUris() { - return [ - [ - 'http://localhost/', - '', - 'http://localhost/' - ], - [ - 'http://localhost/', - 'http://example.com/', - 'http://example.com/' - ], - [ - 'http://localhost/', - 'path', - 'http://localhost/path' - ], - [ - 'http://localhost/', - 'path/', - 'http://localhost/path/' - ], - [ - 'http://localhost/', - 'path//', - 'http://localhost/path/' - ], - [ - 'http://localhost/', - 'path', - 'http://localhost/path' - ], - [ - 'http://localhost/a/b', - '/path', - 'http://localhost/path' - ], - [ - 'http://localhost/', - '/a/b/c', - 'http://localhost/a/b/c' - ], - [ - 'http://localhost/a/path', - 'b/c', - 'http://localhost/a/b/c' - ], - [ - 'http://localhost/a/path', - '/b/c', - 'http://localhost/b/c' - ], - [ - 'http://localhost/a/path/', - 'b/c', - 'http://localhost/a/path/b/c' - ], - [ - 'http://localhost/a/path/', - '../b/c', - 'http://localhost/a/b/c' - ], - [ - 'http://localhost/', - '../../../a/b', - 'http://localhost/a/b' - ], - [ - 'http://localhost/path', - '?query', - 'http://localhost/path?query' - ], - [ - 'http://localhost/path', - '#fragment', - 'http://localhost/path#fragment' - ], - [ - 'http://localhost/path', - 'http://localhost/', - 'http://localhost/' - ], - [ - 'http://localhost/path', - 'http://localhost/?query#fragment', - 'http://localhost/?query#fragment' - ], - [ - 'http://localhost/path/?a#fragment', - '?b', - 'http://localhost/path/?b' - ], - [ - 'http://localhost/path', - '//localhost', - 'http://localhost/' - ], - [ - 'http://localhost/path', - '//localhost/a?query', - 'http://localhost/a?query' - ], - [ - 'http://localhost/path', - '//LOCALHOST', - 'http://localhost/' - ] + yield [ + 'http://localhost/', + '', + 'http://localhost/' + ]; + yield [ + 'http://localhost/', + 'http://example.com/', + 'http://example.com/' + ]; + yield [ + 'http://localhost/', + 'path', + 'http://localhost/path' + ]; + yield [ + 'http://localhost/', + 'path/', + 'http://localhost/path/' + ]; + yield [ + 'http://localhost/', + 'path//', + 'http://localhost/path/' + ]; + yield [ + 'http://localhost/', + 'path', + 'http://localhost/path' + ]; + yield [ + 'http://localhost/a/b', + '/path', + 'http://localhost/path' + ]; + yield [ + 'http://localhost/', + '/a/b/c', + 'http://localhost/a/b/c' + ]; + yield [ + 'http://localhost/a/path', + 'b/c', + 'http://localhost/a/b/c' + ]; + yield [ + 'http://localhost/a/path', + '/b/c', + 'http://localhost/b/c' + ]; + yield [ + 'http://localhost/a/path/', + 'b/c', + 'http://localhost/a/path/b/c' + ]; + yield [ + 'http://localhost/a/path/', + '../b/c', + 'http://localhost/a/b/c' + ]; + yield [ + 'http://localhost/', + '../../../a/b', + 'http://localhost/a/b' + ]; + yield [ + 'http://localhost/path', + '?query', + 'http://localhost/path?query' + ]; + yield [ + 'http://localhost/path', + '#fragment', + 'http://localhost/path#fragment' + ]; + yield [ + 'http://localhost/path', + 'http://localhost/', + 'http://localhost/' + ]; + yield [ + 'http://localhost/path', + 'http://localhost/?query#fragment', + 'http://localhost/?query#fragment' + ]; + yield [ + 'http://localhost/path/?a#fragment', + '?b', + 'http://localhost/path/?b' + ]; + yield [ + 'http://localhost/path', + '//localhost', + 'http://localhost/' + ]; + yield [ + 'http://localhost/path', + '//localhost/a?query', + 'http://localhost/a?query' + ]; + yield [ + 'http://localhost/path', + '//LOCALHOST', + 'http://localhost/' ]; } diff --git a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php index b79826d6..67656d4c 100644 --- a/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php +++ b/tests/Middleware/LimitConcurrentRequestsMiddlewareTest.php @@ -10,6 +10,7 @@ use React\Promise\Deferred; use React\Promise\Promise; use React\Promise\PromiseInterface; +use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; use React\Tests\Http\TestCase; @@ -114,7 +115,8 @@ public function testThrowsExceptionDirectlyFromMiddlewareWhenBelowLimit() { $middleware = new LimitConcurrentRequestsMiddleware(1); - $this->setExpectedException('RuntimeException', 'demo'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('demo'); $middleware(new ServerRequest('GET', 'https://example.com/'), function () { throw new \RuntimeException('demo'); }); @@ -124,7 +126,8 @@ public function testThrowsErrorDirectlyFromMiddlewareWhenBelowLimit() { $middleware = new LimitConcurrentRequestsMiddleware(1); - $this->setExpectedException('Error', 'demo'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('demo'); $middleware(new ServerRequest('GET', 'https://example.com/'), function () { throw new \Error('demo'); }); @@ -159,7 +162,7 @@ public function testReturnsPendingPromiseFromMiddlewareWhenAboveLimit() public function testStreamDoesNotPauseOrResumeWhenBelowLimit() { - $body = $this->getMockBuilder('React\Http\Io\HttpBodyStream')->disableOriginalConstructor()->getMock(); + $body = $this->createMock(HttpBodyStream::class); $body->expects($this->never())->method('pause'); $body->expects($this->never())->method('resume'); $limitHandlers = new LimitConcurrentRequestsMiddleware(1); @@ -168,7 +171,7 @@ public function testStreamDoesNotPauseOrResumeWhenBelowLimit() public function testStreamDoesPauseWhenAboveLimit() { - $body = $this->getMockBuilder('React\Http\Io\HttpBodyStream')->disableOriginalConstructor()->getMock(); + $body = $this->createMock(HttpBodyStream::class); $body->expects($this->once())->method('pause'); $body->expects($this->never())->method('resume'); $limitHandlers = new LimitConcurrentRequestsMiddleware(1); @@ -182,7 +185,7 @@ public function testStreamDoesPauseWhenAboveLimit() public function testStreamDoesPauseAndThenResumeWhenDequeued() { - $body = $this->getMockBuilder('React\Http\Io\HttpBodyStream')->disableOriginalConstructor()->getMock(); + $body = $this->createMock(HttpBodyStream::class); $body->expects($this->once())->method('pause'); $body->expects($this->once())->method('resume'); $limitHandlers = new LimitConcurrentRequestsMiddleware(1); @@ -467,11 +470,11 @@ public function testReceivesStreamingBodyChangesInstanceWithCustomBodyButSameDat $deferred->reject(new \RuntimeException()); $this->assertNotSame($request, $req); - $this->assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $req); + $this->assertInstanceOf(ServerRequestInterface::class, $req); $body = $req->getBody(); - $this->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); - /* @var $body \React\Stream\ReadableStreamInterface */ + $this->assertInstanceOf(ReadableStreamInterface::class, $body); + /* @var $body ReadableStreamInterface */ $this->assertEquals(5, $body->getSize()); diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index 262ad9ca..28866c96 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -272,7 +272,7 @@ function (ServerRequestInterface $request) { $this->assertFalse($stream->isWritable()); assert($exception instanceof \RuntimeException); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); $this->assertEquals('Buffered 3', $exception->getMessage()); $this->assertEquals(42, $exception->getCode()); $this->assertNull($exception->getPrevious()); @@ -307,7 +307,7 @@ function (ServerRequestInterface $request) { $this->assertFalse($stream->isWritable()); assert($exception instanceof \Error); - $this->assertInstanceOf('Error', $exception); + $this->assertInstanceOf(\Error::class, $exception); $this->assertEquals('Buffered 3', $exception->getMessage()); $this->assertEquals(42, $exception->getCode()); $this->assertNull($exception->getPrevious()); @@ -342,10 +342,10 @@ public function testBufferingRejectsWhenStreamEmitsError() $this->assertFalse($stream->isWritable()); assert($exception instanceof \RuntimeException); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $exception); $this->assertEquals('Error while buffering request body: Unexpected Foo', $exception->getMessage()); $this->assertEquals(42, $exception->getCode()); - $this->assertInstanceOf('UnexpectedValueException', $exception->getPrevious()); + $this->assertInstanceOf(\UnexpectedValueException::class, $exception->getPrevious()); } public function testFullBodyStreamedBeforeCallingNextMiddleware() @@ -399,7 +399,7 @@ public function testCancelBufferingClosesStreamAndRejectsPromise() }); assert($exception instanceof \RuntimeException); - $this->assertInstanceOf('RuntimeException', $exception); + $this->assertInstanceOf(\RuntimeException::class, $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 fa6fcd1c..4df6087f 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,6 +2,7 @@ namespace React\Tests\Http; +use PHPUnit\Framework\MockObject\MockBuilder; use PHPUnit\Framework\TestCase as BaseTestCase; class TestCase extends BaseTestCase @@ -39,46 +40,12 @@ protected function expectCallableNever() protected function createCallableMock() { - if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { + if (method_exists(MockBuilder::class, 'addMethods')) { // PHPUnit 9+ - return $this->getMockBuilder('stdClass')->addMethods(['__invoke'])->getMock(); + return $this->getMockBuilder(\stdClass::class)->addMethods(['__invoke'])->getMock(); } else { - // legacy PHPUnit 4 - PHPUnit 8 - return $this->getMockBuilder('stdClass')->setMethods(['__invoke'])->getMock(); + // legacy PHPUnit + return $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock(); } } - - public function assertContainsString($needle, $haystack) - { - if (method_exists($this, 'assertStringContainsString')) { - // PHPUnit 7.5+ - $this->assertStringContainsString($needle, $haystack); - } else { - // legacy PHPUnit 4 - PHPUnit 7.5 - $this->assertContains($needle, $haystack); - } - } - - public function assertNotContainsString($needle, $haystack) - { - if (method_exists($this, 'assertStringNotContainsString')) { - // PHPUnit 7.5+ - $this->assertStringNotContainsString($needle, $haystack); - } else { - // legacy PHPUnit 4 - PHPUnit 7.5 - $this->assertNotContains($needle, $haystack); - } - } - - public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null) - { - $this->expectException($exception); - if ($exceptionMessage !== '') { - $this->expectExceptionMessage($exceptionMessage); - } - if ($exceptionCode !== null) { - $this->expectExceptionCode($exceptionCode); - } - } - } From 905eabfc3c41b229d0a3527e05c7f15f07f3f215 Mon Sep 17 00:00:00 2001 From: James Lucas Date: Wed, 12 Jun 2024 13:41:00 +1000 Subject: [PATCH 149/152] Fix expected error code when ext-sockets is not enabled --- tests/FunctionalBrowserTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index d89d92e9..210cfa50 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -373,7 +373,7 @@ public function testGetRequestWithResponseBufferExceededRejects() $this->expectException(\OverflowException::class); $this->expectExceptionMessage('Response body size of 5 bytes exceeds maximum of 4 bytes'); - $this->expectExceptionCode(defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0); + $this->expectExceptionCode(defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90); await($promise); } @@ -383,7 +383,7 @@ public function testGetRequestWithResponseBufferExceededDuringStreamingRejects() $this->expectException(\OverflowException::class); $this->expectExceptionMessage('Response body size exceeds maximum of 4 bytes'); - $this->expectExceptionCode(defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 0); + $this->expectExceptionCode(defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90); await($promise); } From f1a0406b76e7dce5bc7ecb71e940a0352139fb02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 5 Jul 2024 21:49:14 +0200 Subject: [PATCH 150/152] Improve PHP 8.4+ support by avoiding implicitly nullable types --- composer.json | 10 +++++----- src/Browser.php | 5 +++-- src/Io/Sender.php | 9 ++------- tests/Io/SenderTest.php | 4 +++- tests/Io/StreamingServerTest.php | 4 ++-- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/composer.json b/composer.json index 2fe67da0..809e81c9 100644 --- a/composer.json +++ b/composer.json @@ -31,18 +31,18 @@ "fig/http-message-util": "^1.1", "psr/http-message": "^1.0", "react/event-loop": "^1.2", - "react/promise": "^3 || ^2.3 || ^1.2.1", - "react/socket": "^1.12", - "react/stream": "^1.2" + "react/promise": "^3.2 || ^2.3 || ^1.2.1", + "react/socket": "^1.16", + "react/stream": "^1.4" }, "require-dev": { "clue/http-proxy-react": "^1.8", "clue/reactphp-ssh-proxy": "^1.4", "clue/socks-react": "^1.4", "phpunit/phpunit": "^9.6 || ^7.5", - "react/async": "^4 || ^3", + "react/async": "^4.2 || ^3", "react/promise-stream": "^1.4", - "react/promise-timer": "^1.9" + "react/promise-timer": "^1.11" }, "autoload": { "psr-4": { diff --git a/src/Browser.php b/src/Browser.php index 06e194d9..f042b799 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -10,6 +10,7 @@ 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; @@ -68,11 +69,11 @@ class Browser * @param ?ConnectorInterface $connector * @param ?LoopInterface $loop */ - public function __construct(ConnectorInterface $connector = null, LoopInterface $loop = null) + public function __construct(?ConnectorInterface $connector = null, ?LoopInterface $loop = null) { $loop = $loop ?? Loop::get(); $this->transaction = new Transaction( - Sender::createFromLoop($loop, $connector), + Sender::createFromLoop($loop, $connector ?? new Connector([], $loop)), $loop ); } diff --git a/src/Io/Sender.php b/src/Io/Sender.php index 8ece2ee0..ccb1e1da 100644 --- a/src/Io/Sender.php +++ b/src/Io/Sender.php @@ -8,7 +8,6 @@ use React\Http\Client\Client as HttpClient; use React\Promise\PromiseInterface; use React\Promise\Deferred; -use React\Socket\Connector; use React\Socket\ConnectorInterface; use React\Stream\ReadableStreamInterface; @@ -45,15 +44,11 @@ class Sender * ``` * * @param LoopInterface $loop - * @param ConnectorInterface|null $connector + * @param ConnectorInterface $connector * @return self */ - public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector = null) + public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector) { - if ($connector === null) { - $connector = new Connector([], $loop); - } - return new self(new HttpClient(new ClientConnectionManager($connector, $loop))); } diff --git a/tests/Io/SenderTest.php b/tests/Io/SenderTest.php index df4c5359..4154a38d 100644 --- a/tests/Io/SenderTest.php +++ b/tests/Io/SenderTest.php @@ -34,7 +34,9 @@ public function setUpLoop() public function testCreateFromLoop() { - $sender = Sender::createFromLoop($this->loop, null); + $connector = $this->createMock(ConnectorInterface::class); + + $sender = Sender::createFromLoop($this->loop, $connector); $this->assertInstanceOf(Sender::class, $sender); } diff --git a/tests/Io/StreamingServerTest.php b/tests/Io/StreamingServerTest.php index a61d1425..803ff45c 100644 --- a/tests/Io/StreamingServerTest.php +++ b/tests/Io/StreamingServerTest.php @@ -36,7 +36,7 @@ public function setUpConnectionMockAndSocket() } - private function mockConnection(array $additionalMethods = null) + private function mockConnection(array $additionalMethods = []) { $connection = $this->getMockBuilder(Connection::class) ->disableOriginalConstructor() @@ -53,7 +53,7 @@ private function mockConnection(array $additionalMethods = null) 'getLocalAddress', 'pipe' ], - (is_array($additionalMethods) ? $additionalMethods : []) + $additionalMethods )) ->getMock(); From 559c30d44811a1f048c91c7ea297c0bfa0433368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ana=C3=AFs=20Babel?= Date: Mon, 22 Apr 2024 13:22:45 +0200 Subject: [PATCH 151/152] Allow underscore character in Uri host --- src/Message/Uri.php | 4 ++-- tests/Message/UriTest.php | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Message/Uri.php b/src/Message/Uri.php index 84fc38d8..661f90c4 100644 --- a/src/Message/Uri.php +++ b/src/Message/Uri.php @@ -46,7 +46,7 @@ final class Uri implements UriInterface public function __construct($uri) { $parts = \parse_url(https://github.com/$uri); - if ($parts === false || (isset($parts['scheme']) && !\preg_match('#^[a-z]+$#i', $parts['scheme'])) || (isset($parts['host']) && \preg_match('#[\s_%+]#', $parts['host']))) { + if ($parts === false || (isset($parts['scheme']) && !\preg_match('#^[a-z]+$#i', $parts['scheme'])) || (isset($parts['host']) && \preg_match('#[\s%+]#', $parts['host']))) { throw new \InvalidArgumentException('Invalid URI given'); } @@ -164,7 +164,7 @@ public function withHost($host) return $this; } - if (\preg_match('#[\s_%+]#', $host) || ($host !== '' && \parse_url('http://' . $host, \PHP_URL_HOST) !== $host)) { + if (\preg_match('#[\s%+]#', $host) || ($host !== '' && \parse_url('http://' . $host, \PHP_URL_HOST) !== $host)) { throw new \InvalidArgumentException('Invalid URI host given'); } diff --git a/tests/Message/UriTest.php b/tests/Message/UriTest.php index adaee94b..1774953f 100644 --- a/tests/Message/UriTest.php +++ b/tests/Message/UriTest.php @@ -120,6 +120,9 @@ public static function provideValidUris() yield [ 'http://user%20name:pass%20word@localhost/path%20name?query%20name#frag%20ment' ]; + yield [ + 'http://docker_container/' + ]; } /** @@ -329,6 +332,16 @@ public function testWithHostReturnsNewInstanceWhenHostIsChanged() $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/'); From 6074eefe76a7ada6f902453f0a5ad0147623d049 Mon Sep 17 00:00:00 2001 From: Paul Rotmann Date: Tue, 25 Mar 2025 09:30:14 +0100 Subject: [PATCH 152/152] Run tests on PHP 8.4 and update test environment --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f9cfb41..22793579 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,10 +7,11 @@ on: jobs: PHPUnit: name: PHPUnit (PHP ${{ matrix.php }}) - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: php: + - 8.4 - 8.3 - 8.2 - 8.1 @@ -24,7 +25,7 @@ jobs: - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - coverage: xdebug + coverage: ${{ matrix.php < 8.0 && 'xdebug' || 'pcov' }} ini-file: development - run: composer install - run: vendor/bin/phpunit --coverage-text