Skip to content

Commit abdf36e

Browse files
authored
Merge pull request #216 from WyriHaximus/middleware-buffer
Support buffering request body (RequestBodyBufferMiddleware)
2 parents 2d57b8c + c8d66b8 commit abdf36e

File tree

3 files changed

+241
-7
lines changed

3 files changed

+241
-7
lines changed

README.md

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht
1212
* [Request](#request)
1313
* [Response](#response)
1414
* [Middleware](#middleware)
15+
* [RequestBodyBufferMiddleware](#requestbodybuffermiddleware)
1516
* [Install](#install)
1617
* [Tests](#tests)
1718
* [License](#license)
@@ -237,8 +238,9 @@ and
237238
`Server`, but you can add these parameters by yourself using the given methods.
238239
The next versions of this project will cover these features.
239240

240-
Note that the request object will be processed once the request headers have
241-
been received.
241+
Note that by default, the request object will be processed once the request headers have
242+
been received (see also [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware)
243+
for an alternative).
242244
This means that this happens irrespective of (i.e. *before*) receiving the
243245
(potentially much larger) request body.
244246
While this may be uncommon in the PHP ecosystem, this is actually a very powerful
@@ -253,16 +255,18 @@ approach that gives you several advantages not otherwise possible:
253255
such as accepting a huge file upload or possibly unlimited request body stream.
254256

255257
The `getBody()` method can be used to access the request body stream.
256-
This method returns a stream instance that implements both the
258+
In the default streaming mode, this method returns a stream instance that implements both the
257259
[PSR-7 StreamInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagestreaminterface)
258260
and the [ReactPHP ReadableStreamInterface](https://github.com/reactphp/stream#readablestreaminterface).
259261
However, most of the `PSR-7 StreamInterface` methods have been
260262
designed under the assumption of being in control of the request body.
261263
Given that this does not apply to this server, the following
262264
`PSR-7 StreamInterface` methods are not used and SHOULD NOT be called:
263265
`tell()`, `eof()`, `seek()`, `rewind()`, `write()` and `read()`.
264-
Instead, you should use the `ReactPHP ReadableStreamInterface` which
265-
gives you access to the incoming request body as the individual chunks arrive:
266+
If this is an issue for your use case, it's highly recommended to use the
267+
[`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) instead.
268+
The `ReactPHP ReadableStreamInterface` gives you access to the incoming
269+
request body as the individual chunks arrive:
266270

267271
```php
268272
$server = new Server(function (ServerRequestInterface $request) {
@@ -297,7 +301,7 @@ $server = new Server(function (ServerRequestInterface $request) {
297301
The above example simply counts the number of bytes received in the request body.
298302
This can be used as a skeleton for buffering or processing the request body.
299303

300-
See also [example #4](examples) for more details.
304+
See also [example #9](examples) for more details.
301305

302306
The `data` event will be emitted whenever new data is available on the request
303307
body stream.
@@ -414,7 +418,7 @@ non-alphanumeric characters.
414418
This encoding is also used internally when decoding the name and value of cookies
415419
(which is in line with other implementations, such as PHP's cookie functions).
416420

417-
See also [example #6](examples) for more details.
421+
See also [example #5](examples) for more details.
418422

419423
### Response
420424

@@ -677,6 +681,50 @@ $server = new Server(new MiddlewareRunner([
677681
]));
678682
```
679683

684+
#### RequestBodyBufferMiddleware
685+
686+
One of the built-in middleware is the `RequestBodyBufferMiddleware` which
687+
can be used to buffer the whole incoming request body in memory.
688+
This can be useful if full PSR-7 compatibility is needed for the request handler
689+
and the default streaming request body handling is not needed.
690+
The constructor accepts one optional argument, the maximum request body size.
691+
When one isn't provided it will use `post_max_size` (default 8 MiB) from PHP's
692+
configuration.
693+
(Note that the value from your matching SAPI will be used, which is the CLI
694+
configuration in most cases.)
695+
696+
Any incoming request that has a request body that exceeds this limit will be
697+
rejected with a `413` (Request Entity Too Large) error message without calling
698+
the next middleware handlers.
699+
700+
Any incoming request that does not have its size defined and uses the (rare)
701+
`Transfer-Encoding: chunked` will be rejected with a `411` (Length Required)
702+
error message without calling the next middleware handlers.
703+
Note that this only affects incoming requests, the much more common chunked
704+
transfer encoding for outgoing responses is not affected.
705+
It is recommended to define a `Content-Length` header instead.
706+
Note that this does not affect normal requests without a request body
707+
(such as a simple `GET` request).
708+
709+
All other requests will be buffered in memory until the request body end has
710+
been reached and then call the next middleware handler with the complete,
711+
buffered request.
712+
Similarly, this will immediately invoke the next middleware handler for requests
713+
that have an empty request body (such as a simple `GET` request) and requests
714+
that are already buffered (such as due to another middleware).
715+
716+
Usage:
717+
718+
```php
719+
$middlewares = new MiddlewareRunner([
720+
new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB
721+
function (ServerRequestInterface $request, callable $next) {
722+
// The body from $request->getBody() is now fully available without the need to stream it
723+
return new Response(200);
724+
},
725+
]);
726+
```
727+
680728
## Install
681729

682730
The recommended way to install this library is [through Composer](http://getcomposer.org).
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
namespace React\Http\Middleware;
4+
5+
use Psr\Http\Message\ServerRequestInterface;
6+
use React\Http\Response;
7+
use React\Promise\Stream;
8+
use React\Stream\ReadableStreamInterface;
9+
use RingCentral\Psr7\BufferStream;
10+
11+
final class RequestBodyBufferMiddleware
12+
{
13+
private $sizeLimit;
14+
15+
/**
16+
* @param int|null $sizeLimit Either an int with the max request body size
17+
* or null to use post_max_size from PHP's
18+
* configuration. (Note that the value from
19+
* the CLI configuration will be used.)
20+
*/
21+
public function __construct($sizeLimit = null)
22+
{
23+
if ($sizeLimit === null) {
24+
$sizeLimit = $this->iniMaxPostSize();
25+
}
26+
27+
$this->sizeLimit = $sizeLimit;
28+
}
29+
30+
public function __invoke(ServerRequestInterface $request, $stack)
31+
{
32+
$size = $request->getBody()->getSize();
33+
34+
if ($size === null) {
35+
return new Response(411, array('Content-Type' => 'text/plain'), 'No Content-Length header given');
36+
}
37+
38+
if ($size > $this->sizeLimit) {
39+
return new Response(413, array('Content-Type' => 'text/plain'), 'Request body exceeds allowed limit');
40+
}
41+
42+
$body = $request->getBody();
43+
if (!$body instanceof ReadableStreamInterface) {
44+
return $stack($request);
45+
}
46+
47+
return Stream\buffer($body)->then(function ($buffer) use ($request, $stack) {
48+
$stream = new BufferStream(strlen($buffer));
49+
$stream->write($buffer);
50+
$request = $request->withBody($stream);
51+
52+
return $stack($request);
53+
});
54+
}
55+
56+
/**
57+
* Gets post_max_size from PHP's configuration expressed in bytes
58+
*
59+
* @return int
60+
* @link http://php.net/manual/en/ini.core.php#ini.post-max-size
61+
* @codeCoverageIgnore
62+
*/
63+
private function iniMaxPostSize()
64+
{
65+
$size = ini_get('post_max_size');
66+
$suffix = strtoupper(substr($size, -1));
67+
if ($suffix === 'K') {
68+
return substr($size, 0, -1) * 1024;
69+
}
70+
if ($suffix === 'M') {
71+
return substr($size, 0, -1) * 1024 * 1024;
72+
}
73+
if ($suffix === 'G') {
74+
return substr($size, 0, -1) * 1024 * 1024 * 1024;
75+
}
76+
77+
return $size;
78+
}
79+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
namespace React\Tests\Http\Middleware;
4+
5+
use Psr\Http\Message\ServerRequestInterface;
6+
use React\Http\Middleware\RequestBodyBufferMiddleware;
7+
use React\Http\ServerRequest;
8+
use React\Tests\Http\TestCase;
9+
use RingCentral\Psr7\BufferStream;
10+
use React\Stream\ThroughStream;
11+
use React\Http\HttpBodyStream;
12+
13+
final class RequestBodyBufferMiddlewareTest extends TestCase
14+
{
15+
public function testBufferingResolvesWhenStreamEnds()
16+
{
17+
$stream = new ThroughStream();
18+
$serverRequest = new ServerRequest(
19+
'GET',
20+
'https://example.com/',
21+
array(),
22+
new HttpBodyStream($stream, 11)
23+
);
24+
25+
$exposedRequest = null;
26+
$buffer = new RequestBodyBufferMiddleware(20);
27+
$buffer(
28+
$serverRequest,
29+
function (ServerRequestInterface $request) use (&$exposedRequest) {
30+
$exposedRequest = $request;
31+
}
32+
);
33+
34+
$stream->write('hello');
35+
$stream->write('world');
36+
$stream->end('!');
37+
38+
$this->assertSame('helloworld!', $exposedRequest->getBody()->getContents());
39+
}
40+
41+
public function testAlreadyBufferedResolvesImmediately()
42+
{
43+
$size = 1024;
44+
$body = str_repeat('x', $size);
45+
$stream = new BufferStream($size);
46+
$stream->write($body);
47+
$serverRequest = new ServerRequest(
48+
'GET',
49+
'https://example.com/',
50+
array(),
51+
$stream
52+
);
53+
54+
$exposedRequest = null;
55+
$buffer = new RequestBodyBufferMiddleware();
56+
$buffer(
57+
$serverRequest,
58+
function (ServerRequestInterface $request) use (&$exposedRequest) {
59+
$exposedRequest = $request;
60+
}
61+
);
62+
63+
$this->assertSame($body, $exposedRequest->getBody()->getContents());
64+
}
65+
66+
public function testUnknownSizeReturnsError411()
67+
{
68+
$body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock();
69+
$body->expects($this->once())->method('getSize')->willReturn(null);
70+
71+
$serverRequest = new ServerRequest(
72+
'GET',
73+
'https://example.com/',
74+
array(),
75+
$body
76+
);
77+
78+
$buffer = new RequestBodyBufferMiddleware();
79+
$response = $buffer(
80+
$serverRequest,
81+
function () {}
82+
);
83+
84+
$this->assertSame(411, $response->getStatusCode());
85+
}
86+
87+
public function testExcessiveSizeReturnsError413()
88+
{
89+
$stream = new BufferStream(2);
90+
$stream->write('aa');
91+
92+
$serverRequest = new ServerRequest(
93+
'GET',
94+
'https://example.com/',
95+
array(),
96+
$stream
97+
);
98+
99+
$buffer = new RequestBodyBufferMiddleware(1);
100+
$response = $buffer(
101+
$serverRequest,
102+
function () {}
103+
);
104+
105+
$this->assertSame(413, $response->getStatusCode());
106+
}
107+
}

0 commit comments

Comments
 (0)