Skip to content

Commit 7b08b2c

Browse files
authored
Merge pull request #395 from clue-labs/buffer-request
Keep request body in memory also after consuming request body
2 parents 754b0c1 + 6fec25b commit 7b08b2c

File tree

4 files changed

+546
-9
lines changed

4 files changed

+546
-9
lines changed

src/Io/BufferedBody.php

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<?php
2+
3+
namespace React\Http\Io;
4+
5+
use Psr\Http\Message\StreamInterface;
6+
7+
/**
8+
* [Internal] PSR-7 message body implementation using an in-memory buffer
9+
*
10+
* @internal
11+
*/
12+
class BufferedBody implements StreamInterface
13+
{
14+
private $buffer = '';
15+
private $position = 0;
16+
private $closed = false;
17+
18+
public function __construct($buffer)
19+
{
20+
$this->buffer = $buffer;
21+
}
22+
23+
public function __toString()
24+
{
25+
if ($this->closed) {
26+
return '';
27+
}
28+
29+
$this->seek(0);
30+
31+
return $this->getContents();
32+
}
33+
34+
public function close()
35+
{
36+
$this->buffer = '';
37+
$this->position = 0;
38+
$this->closed = true;
39+
}
40+
41+
public function detach()
42+
{
43+
$this->close();
44+
45+
return null;
46+
}
47+
48+
public function getSize()
49+
{
50+
return $this->closed ? null : \strlen($this->buffer);
51+
}
52+
53+
public function tell()
54+
{
55+
if ($this->closed) {
56+
throw new \RuntimeException('Unable to tell position of closed stream');
57+
}
58+
59+
return $this->position;
60+
}
61+
62+
public function eof()
63+
{
64+
return $this->position >= \strlen($this->buffer);
65+
}
66+
67+
public function isSeekable()
68+
{
69+
return !$this->closed;
70+
}
71+
72+
public function seek($offset, $whence = \SEEK_SET)
73+
{
74+
if ($this->closed) {
75+
throw new \RuntimeException('Unable to seek on closed stream');
76+
}
77+
78+
$old = $this->position;
79+
80+
if ($whence === \SEEK_SET) {
81+
$this->position = $offset;
82+
} elseif ($whence === \SEEK_CUR) {
83+
$this->position += $offset;
84+
} elseif ($whence === \SEEK_END) {
85+
$this->position = \strlen($this->buffer) + $offset;
86+
} else {
87+
throw new \InvalidArgumentException('Invalid seek mode given');
88+
}
89+
90+
if (!\is_int($this->position) || $this->position < 0) {
91+
$this->position = $old;
92+
throw new \RuntimeException('Unable to seek to position');
93+
}
94+
}
95+
96+
public function rewind()
97+
{
98+
$this->seek(0);
99+
}
100+
101+
public function isWritable()
102+
{
103+
return !$this->closed;
104+
}
105+
106+
public function write($string)
107+
{
108+
if ($this->closed) {
109+
throw new \RuntimeException('Unable to write to closed stream');
110+
}
111+
112+
if ($string === '') {
113+
return 0;
114+
}
115+
116+
if ($this->position > 0 && !isset($this->buffer[$this->position - 1])) {
117+
$this->buffer = \str_pad($this->buffer, $this->position, "\0");
118+
}
119+
120+
$len = \strlen($string);
121+
$this->buffer = \substr($this->buffer, 0, $this->position) . $string . \substr($this->buffer, $this->position + $len);
122+
$this->position += $len;
123+
124+
return $len;
125+
}
126+
127+
public function isReadable()
128+
{
129+
return !$this->closed;
130+
}
131+
132+
public function read($length)
133+
{
134+
if ($this->closed) {
135+
throw new \RuntimeException('Unable to read from closed stream');
136+
}
137+
138+
if ($length < 1) {
139+
throw new \InvalidArgumentException('Invalid read length given');
140+
}
141+
142+
if ($this->position + $length > \strlen($this->buffer)) {
143+
$length = \strlen($this->buffer) - $this->position;
144+
}
145+
146+
if (!isset($this->buffer[$this->position])) {
147+
return '';
148+
}
149+
150+
$pos = $this->position;
151+
$this->position += $length;
152+
153+
return \substr($this->buffer, $pos, $length);
154+
}
155+
156+
public function getContents()
157+
{
158+
if ($this->closed) {
159+
throw new \RuntimeException('Unable to read from closed stream');
160+
}
161+
162+
if (!isset($this->buffer[$this->position])) {
163+
return '';
164+
}
165+
166+
$pos = $this->position;
167+
$this->position = \strlen($this->buffer);
168+
169+
return \substr($this->buffer, $pos);
170+
}
171+
172+
public function getMetadata($key = null)
173+
{
174+
return $key === null ? array() : null;
175+
}
176+
}

src/Middleware/RequestBodyBufferMiddleware.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
use OverflowException;
66
use Psr\Http\Message\ServerRequestInterface;
7+
use React\Http\Io\BufferedBody;
78
use React\Http\Io\IniUtil;
89
use React\Promise\Stream;
910
use React\Stream\ReadableStreamInterface;
10-
use RingCentral\Psr7\BufferStream;
1111

1212
final class RequestBodyBufferMiddleware
1313
{
@@ -38,7 +38,7 @@ public function __invoke(ServerRequestInterface $request, $stack)
3838
if ($size === 0 || !$body instanceof ReadableStreamInterface) {
3939
// replace with empty body if body is streaming (or buffered size exceeds limit)
4040
if ($body instanceof ReadableStreamInterface || $size > $this->sizeLimit) {
41-
$request = $request->withBody(new BufferStream(0));
41+
$request = $request->withBody(new BufferedBody(''));
4242
}
4343

4444
return $stack($request);
@@ -51,9 +51,7 @@ public function __invoke(ServerRequestInterface $request, $stack)
5151
}
5252

5353
return Stream\buffer($body, $sizeLimit)->then(function ($buffer) use ($request, $stack) {
54-
$stream = new BufferStream(\strlen($buffer));
55-
$stream->write($buffer);
56-
$request = $request->withBody($stream);
54+
$request = $request->withBody(new BufferedBody($buffer));
5755

5856
return $stack($request);
5957
}, function ($error) use ($stack, $request, $body) {

0 commit comments

Comments
 (0)