Skip to content

Commit

Permalink
Improve documentation for buffering the request body
Browse files Browse the repository at this point in the history
  • Loading branch information
clue committed Sep 11, 2017
1 parent 604d79a commit 4c4e950
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 30 deletions.
58 changes: 39 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -254,16 +255,18 @@ 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
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) {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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);
Expand Down
7 changes: 3 additions & 4 deletions src/Middleware/RequestBodyBufferMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
42 changes: 35 additions & 7 deletions tests/Middleware/RequestBodyBufferMiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -24,7 +52,7 @@ public function testBuffer()
);

$exposedRequest = null;
$buffer = new RequestBodyBuffer();
$buffer = new RequestBodyBufferMiddleware();
$buffer(
$serverRequest,
function (ServerRequestInterface $request) use (&$exposedRequest) {
Expand All @@ -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);
Expand All @@ -47,7 +75,7 @@ public function test411Error()
$body
);

$buffer = new RequestBodyBuffer();
$buffer = new RequestBodyBufferMiddleware();
$response = $buffer(
$serverRequest,
function () {}
Expand All @@ -56,7 +84,7 @@ function () {}
$this->assertSame(411, $response->getStatusCode());
}

public function test413Error()
public function testExcessiveSizeReturnsError413()
{
$stream = new BufferStream(2);
$stream->write('aa');
Expand All @@ -68,7 +96,7 @@ public function test413Error()
$stream
);

$buffer = new RequestBodyBuffer(1);
$buffer = new RequestBodyBufferMiddleware(1);
$response = $buffer(
$serverRequest,
function () {}
Expand Down

0 comments on commit 4c4e950

Please sign in to comment.