Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support buffering request body (RequestBodyBufferMiddleware) #216

Merged
merged 4 commits into from
Sep 11, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 55 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht
* [Request](#request)
* [Response](#response)
* [Middleware](#middleware)
* [RequestBodyBufferMiddleware](#requestbodybuffermiddleware)
* [Install](#install)
* [Tests](#tests)
* [License](#license)
Expand Down Expand Up @@ -237,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 @@ -253,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 @@ -297,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 @@ -414,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 @@ -677,6 +681,50 @@ $server = new Server(new MiddlewareRunner([
]));
```

#### RequestBodyBufferMiddleware

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 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, the 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).

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 (such as due to another middleware).

Usage:

```php
$middlewares = new MiddlewareRunner([
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);
},
]);
```

## Install

The recommended way to install this library is [through Composer](http://getcomposer.org).
Expand Down
79 changes: 79 additions & 0 deletions src/Middleware/RequestBodyBufferMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace React\Http\Middleware;

use Psr\Http\Message\ServerRequestInterface;
use React\Http\Response;
use React\Promise\Stream;
use React\Stream\ReadableStreamInterface;
use RingCentral\Psr7\BufferStream;

final class RequestBodyBufferMiddleware
{
private $sizeLimit;

/**
* @param int|null $sizeLimit Either an int with the max request body size
* or null to use post_max_size from PHP's
* configuration. (Note that the value from
* the CLI configuration will be used.)
*/
public function __construct($sizeLimit = null)
{
if ($sizeLimit === null) {
$sizeLimit = $this->iniMaxPostSize();
}

$this->sizeLimit = $sizeLimit;
}

public function __invoke(ServerRequestInterface $request, $stack)
{
$size = $request->getBody()->getSize();

if ($size === null) {
return new Response(411, array('Content-Type' => 'text/plain'), 'No Content-Length header given');
}

if ($size > $this->sizeLimit) {
return new Response(413, array('Content-Type' => 'text/plain'), 'Request body exceeds allowed limit');
}

$body = $request->getBody();
if (!$body instanceof ReadableStreamInterface) {
return $stack($request);
}

return Stream\buffer($body)->then(function ($buffer) use ($request, $stack) {
$stream = new BufferStream(strlen($buffer));
$stream->write($buffer);
$request = $request->withBody($stream);

return $stack($request);
});
}

/**
* 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()
{
$size = ini_get('post_max_size');
$suffix = strtoupper(substr($size, -1));
if ($suffix === 'K') {
return substr($size, 0, -1) * 1024;
}
if ($suffix === 'M') {
return substr($size, 0, -1) * 1024 * 1024;
}
if ($suffix === 'G') {
return substr($size, 0, -1) * 1024 * 1024 * 1024;
}

return $size;
}
}
107 changes: 107 additions & 0 deletions tests/Middleware/RequestBodyBufferMiddlewareTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

namespace React\Tests\Http\Middleware;

use Psr\Http\Message\ServerRequestInterface;
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 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);
$stream = new BufferStream($size);
$stream->write($body);
$serverRequest = new ServerRequest(
'GET',
'https://example.com/',
array(),
$stream
);

$exposedRequest = null;
$buffer = new RequestBodyBufferMiddleware();
$buffer(
$serverRequest,
function (ServerRequestInterface $request) use (&$exposedRequest) {
$exposedRequest = $request;
}
);

$this->assertSame($body, $exposedRequest->getBody()->getContents());
}

public function testUnknownSizeReturnsError411()
{
$body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock();
$body->expects($this->once())->method('getSize')->willReturn(null);

$serverRequest = new ServerRequest(
'GET',
'https://example.com/',
array(),
$body
);

$buffer = new RequestBodyBufferMiddleware();
$response = $buffer(
$serverRequest,
function () {}
);

$this->assertSame(411, $response->getStatusCode());
}

public function testExcessiveSizeReturnsError413()
{
$stream = new BufferStream(2);
$stream->write('aa');

$serverRequest = new ServerRequest(
'GET',
'https://example.com/',
array(),
$stream
);

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

$this->assertSame(413, $response->getStatusCode());
}
}