diff --git a/README.md b/README.md index 510cdf38..cba030a0 100644 --- a/README.md +++ b/README.md @@ -238,8 +238,9 @@ and `Server`, but you can add these parameters by yourself using the given methods. The next versions of this project will cover these features. -Note that the request object will be processed once the request headers have -been received. +Note that by default, the request object will be processed once the request headers have +been received (see also [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) +for an alternative). This means that this happens irrespective of (i.e. *before*) receiving the (potentially much larger) request body. While this may be uncommon in the PHP ecosystem, this is actually a very powerful @@ -254,7 +255,7 @@ approach that gives you several advantages not otherwise possible: such as accepting a huge file upload or possibly unlimited request body stream. The `getBody()` method can be used to access the request body stream. -This method returns a stream instance that implements both the +In the default streaming mode, this method returns a stream instance that implements both the [PSR-7 StreamInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagestreaminterface) and the [ReactPHP ReadableStreamInterface](https://github.com/reactphp/stream#readablestreaminterface). However, most of the `PSR-7 StreamInterface` methods have been @@ -262,8 +263,10 @@ designed under the assumption of being in control of the 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: `tell()`, `eof()`, `seek()`, `rewind()`, `write()` and `read()`. -Instead, you should use the `ReactPHP ReadableStreamInterface` which -gives you access to the incoming request body as the individual chunks arrive: +If this is an issue for your use case, it's highly recommended to use the +[`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) instead. +The `ReactPHP ReadableStreamInterface` gives you access to the incoming +request body as the individual chunks arrive: ```php $server = new Server(function (ServerRequestInterface $request) { @@ -298,7 +301,7 @@ $server = new Server(function (ServerRequestInterface $request) { 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 #4](examples) for more details. +See also [example #9](examples) for more details. The `data` event will be emitted whenever new data is available on the request body stream. @@ -415,7 +418,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 #6](examples) for more details. +See also [example #5](examples) for more details. ### Response @@ -680,24 +683,41 @@ $server = new Server(new MiddlewareRunner([ #### RequestBodyBufferMiddleware -One of the build in middleware is `RequestBodyBufferMiddleware` which will buffer the incoming -request body until the reported size has been reached. Then it will call the next -middleware in line with the new request instance containing the full request body. -The constructor accepts one argument, a maximum request body size. When one isn't -provided it will use `post_max_size` from PHP's configuration. -(**Note that the value from the CLI configuration will be used.**) +One of the built-in middleware is the `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. +The constructor accepts one optional argument, the maximum request body size. +When one isn't provided it will use `post_max_size` (default 8 MiB) from PHP's +configuration. +(Note that the value from your matching SAPI will be used, which is the CLI +configuration in most cases.) + +Any incoming request that has a size that has a request body that exceeds this +limit will be rejected with a `413` (Request Entity Too Large) error message +without calling the next middleware handlers. + +Any incoming request that does not have its size defined and uses the (rare) +`Transfer-Encoding: chunked` will be rejected with a `411` (Length Required) +error message without calling the next middleware handlers. +Note that this only affects incoming requests, much more common chunked transfer +encoding for outgoing responses is not affected. +It is recommended to define a `Content-Length` header instead. +Note that this does not affect normal requests without a request body +(such as a simple `GET` request). -Before buffering the request body the `RequestBodyBufferMiddleware` will check if the request -body has a size. When size is null a [HTTP 411](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/411) -response will be send to the client. When size is bigger then supplied to the `RequestBodyBufferMiddleware` -constructor or taken from `post_max_size` a [HTTP 413](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413) -response will be dispatched. +All other requests will be buffered in memory until the request body end has +been reached and then call the next middleware handler with the complete, +buffered request. +Similarly, this will immediately invoke the next middleware handler for requests +that have an empty request body (such as a simple `GET` request) and requests +that are already buffered due to another middleware. Usage: ```php $middlewares = new MiddlewareRunner([ - new RequestBodyBufferMiddleware(), + new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB function (ServerRequestInterface $request, callable $next) { // The body from $request->getBody() is now fully available without the need to stream it return new Response(200); diff --git a/src/Middleware/RequestBodyBufferMiddleware.php b/src/Middleware/RequestBodyBufferMiddleware.php index 9bb11750..93ba2126 100644 --- a/src/Middleware/RequestBodyBufferMiddleware.php +++ b/src/Middleware/RequestBodyBufferMiddleware.php @@ -54,12 +54,11 @@ public function __invoke(ServerRequestInterface $request, $stack) } /** - * Gets post_max_size from PHP's configuration - * and turns it into bytes up to a maximum of GigaBytes. - * Anything other than configured as Bytes, KiloBytes, MegaBytes, or GigaBytes - * is considered out of range. + * Gets post_max_size from PHP's configuration expressed in bytes * * @return int + * @link http://php.net/manual/en/ini.core.php#ini.post-max-size + * @codeCoverageIgnore */ private function iniMaxPostSize() { diff --git a/tests/Middleware/RequestBodyBufferMiddlewareTest.php b/tests/Middleware/RequestBodyBufferMiddlewareTest.php index 3468e0dc..337f5c03 100644 --- a/tests/Middleware/RequestBodyBufferMiddlewareTest.php +++ b/tests/Middleware/RequestBodyBufferMiddlewareTest.php @@ -3,14 +3,42 @@ namespace React\Tests\Http\Middleware; use Psr\Http\Message\ServerRequestInterface; -use React\Http\Middleware\RequestBodyBuffer; +use React\Http\Middleware\RequestBodyBufferMiddleware; use React\Http\ServerRequest; use React\Tests\Http\TestCase; use RingCentral\Psr7\BufferStream; +use React\Stream\ThroughStream; +use React\Http\HttpBodyStream; final class RequestBodyBufferMiddlewareTest extends TestCase { - public function testBuffer() + public function testBufferingResolvesWhenStreamEnds() + { + $stream = new ThroughStream(); + $serverRequest = new ServerRequest( + 'GET', + 'https://example.com/', + array(), + new HttpBodyStream($stream, 11) + ); + + $exposedRequest = null; + $buffer = new RequestBodyBufferMiddleware(20); + $buffer( + $serverRequest, + function (ServerRequestInterface $request) use (&$exposedRequest) { + $exposedRequest = $request; + } + ); + + $stream->write('hello'); + $stream->write('world'); + $stream->end('!'); + + $this->assertSame('helloworld!', $exposedRequest->getBody()->getContents()); + } + + public function testAlreadyBufferedResolvesImmediately() { $size = 1024; $body = str_repeat('x', $size); @@ -24,7 +52,7 @@ public function testBuffer() ); $exposedRequest = null; - $buffer = new RequestBodyBuffer(); + $buffer = new RequestBodyBufferMiddleware(); $buffer( $serverRequest, function (ServerRequestInterface $request) use (&$exposedRequest) { @@ -35,7 +63,7 @@ function (ServerRequestInterface $request) use (&$exposedRequest) { $this->assertSame($body, $exposedRequest->getBody()->getContents()); } - public function test411Error() + public function testUnknownSizeReturnsError411() { $body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock(); $body->expects($this->once())->method('getSize')->willReturn(null); @@ -47,7 +75,7 @@ public function test411Error() $body ); - $buffer = new RequestBodyBuffer(); + $buffer = new RequestBodyBufferMiddleware(); $response = $buffer( $serverRequest, function () {} @@ -56,7 +84,7 @@ function () {} $this->assertSame(411, $response->getStatusCode()); } - public function test413Error() + public function testExcessiveSizeReturnsError413() { $stream = new BufferStream(2); $stream->write('aa'); @@ -68,7 +96,7 @@ public function test413Error() $stream ); - $buffer = new RequestBodyBuffer(1); + $buffer = new RequestBodyBufferMiddleware(1); $response = $buffer( $serverRequest, function () {}