Skip to content

Commit 1d5bd0b

Browse files
authored
Merge pull request #235 from andig/chunked
Support buffering requests with unknown length (Transfer-Encoding: chunked) in RequestBodyBufferMiddleware
2 parents 01ae9f7 + cbf14eb commit 1d5bd0b

File tree

4 files changed

+78
-32
lines changed

4 files changed

+78
-32
lines changed

README.md

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -697,16 +697,11 @@ Any incoming request that has a request body that exceeds this limit will be
697697
rejected with a `413` (Request Entity Too Large) error message without calling
698698
the next middleware handlers.
699699

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).
700+
The `RequestBodyBufferMiddleware` will buffer requests with bodies of known size
701+
(i.e. with `Content-Length` header specified) as well as requests with bodies of
702+
unknown size (i.e. with `Transfer-Encoding: chunked` header).
708703

709-
All other requests will be buffered in memory until the request body end has
704+
All requests will be buffered in memory until the request body end has
710705
been reached and then call the next middleware handler with the complete,
711706
buffered request.
712707
Similarly, this will immediately invoke the next middleware handler for requests

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"react/stream": "^1.0 || ^0.7.1",
1111
"react/promise": "^2.3 || ^1.2.1",
1212
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
13-
"react/promise-stream": "^0.1.1"
13+
"react/promise-stream": "^1.0 || ^0.1.2"
1414
},
1515
"autoload": {
1616
"psr-4": {

src/Middleware/RequestBodyBufferMiddleware.php

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use React\Promise\Stream;
88
use React\Stream\ReadableStreamInterface;
99
use RingCentral\Psr7\BufferStream;
10+
use OverflowException;
1011

1112
final class RequestBodyBufferMiddleware
1213
{
@@ -29,27 +30,30 @@ public function __construct($sizeLimit = null)
2930

3031
public function __invoke(ServerRequestInterface $request, $stack)
3132
{
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-
}
33+
$body = $request->getBody();
3734

38-
if ($size > $this->sizeLimit) {
35+
// request body of known size exceeding limit
36+
if ($body->getSize() > $this->sizeLimit) {
3937
return new Response(413, array('Content-Type' => 'text/plain'), 'Request body exceeds allowed limit');
4038
}
4139

42-
$body = $request->getBody();
4340
if (!$body instanceof ReadableStreamInterface) {
4441
return $stack($request);
4542
}
4643

47-
return Stream\buffer($body)->then(function ($buffer) use ($request, $stack) {
44+
return Stream\buffer($body, $this->sizeLimit)->then(function ($buffer) use ($request, $stack) {
4845
$stream = new BufferStream(strlen($buffer));
4946
$stream->write($buffer);
5047
$request = $request->withBody($stream);
5148

5249
return $stack($request);
50+
}, function($error) {
51+
// request body of unknown size exceeding limit during buffering
52+
if ($error instanceof OverflowException) {
53+
return new Response(413, array('Content-Type' => 'text/plain'), 'Request body exceeds allowed limit');
54+
}
55+
56+
throw $error;
5357
});
5458
}
5559

tests/Middleware/RequestBodyBufferMiddlewareTest.php

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
namespace React\Tests\Http\Middleware;
44

55
use Psr\Http\Message\ServerRequestInterface;
6+
use React\EventLoop\Factory;
67
use React\Http\Io\HttpBodyStream;
78
use React\Http\Io\ServerRequest;
89
use React\Http\Middleware\RequestBodyBufferMiddleware;
910
use React\Stream\ThroughStream;
1011
use React\Tests\Http\TestCase;
1112
use RingCentral\Psr7\BufferStream;
13+
use Clue\React\Block;
1214

1315
final class RequestBodyBufferMiddlewareTest extends TestCase
1416
{
@@ -63,45 +65,90 @@ function (ServerRequestInterface $request) use (&$exposedRequest) {
6365
$this->assertSame($body, $exposedRequest->getBody()->getContents());
6466
}
6567

66-
public function testUnknownSizeReturnsError411()
68+
public function testExcessiveSizeImmediatelyReturnsError413ForKnownSize()
6769
{
68-
$body = $this->getMockBuilder('Psr\Http\Message\StreamInterface')->getMock();
69-
$body->expects($this->once())->method('getSize')->willReturn(null);
70-
70+
$loop = Factory::create();
71+
72+
$stream = new ThroughStream();
73+
$stream->end('aa');
7174
$serverRequest = new ServerRequest(
7275
'GET',
7376
'https://example.com/',
7477
array(),
75-
$body
78+
new HttpBodyStream($stream, 2)
7679
);
7780

78-
$buffer = new RequestBodyBufferMiddleware();
81+
$buffer = new RequestBodyBufferMiddleware(1);
7982
$response = $buffer(
8083
$serverRequest,
81-
function () {}
84+
function (ServerRequestInterface $request) {
85+
return $request;
86+
}
8287
);
8388

84-
$this->assertSame(411, $response->getStatusCode());
89+
$this->assertSame(413, $response->getStatusCode());
8590
}
8691

8792
public function testExcessiveSizeReturnsError413()
8893
{
89-
$stream = new BufferStream(2);
90-
$stream->write('aa');
94+
$loop = Factory::create();
9195

96+
$stream = new ThroughStream();
9297
$serverRequest = new ServerRequest(
9398
'GET',
9499
'https://example.com/',
95100
array(),
96-
$stream
101+
new HttpBodyStream($stream, null)
97102
);
98103

99104
$buffer = new RequestBodyBufferMiddleware(1);
100-
$response = $buffer(
105+
$promise = $buffer(
101106
$serverRequest,
102-
function () {}
107+
function (ServerRequestInterface $request) {
108+
return $request;
109+
}
103110
);
104111

105-
$this->assertSame(413, $response->getStatusCode());
112+
$stream->end('aa');
113+
114+
$exposedResponse = null;
115+
$promise->then(
116+
function($response) use (&$exposedResponse) {
117+
$exposedResponse = $response;
118+
},
119+
$this->expectCallableNever()
120+
);
121+
122+
$this->assertSame(413, $exposedResponse->getStatusCode());
123+
124+
Block\await($promise, $loop);
125+
}
126+
127+
/**
128+
* @expectedException RuntimeException
129+
*/
130+
public function testBufferingErrorThrows()
131+
{
132+
$loop = Factory::create();
133+
134+
$stream = new ThroughStream();
135+
$serverRequest = new ServerRequest(
136+
'GET',
137+
'https://example.com/',
138+
array(),
139+
new HttpBodyStream($stream, null)
140+
);
141+
142+
$buffer = new RequestBodyBufferMiddleware(1);
143+
$promise = $buffer(
144+
$serverRequest,
145+
function (ServerRequestInterface $request) {
146+
return $request;
147+
}
148+
);
149+
150+
$stream->emit('error', array(new \RuntimeException()));
151+
152+
Block\await($promise, $loop);
106153
}
107154
}

0 commit comments

Comments
 (0)