Skip to content
This repository was archived by the owner on Jan 29, 2020. It is now read-only.

Commit a8be92d

Browse files
fcabralpachecoOcramius
authored andcommitted
Fix issue with high memory consumption in readable streams.
1 parent 972b0e1 commit a8be92d

File tree

2 files changed

+126
-20
lines changed

2 files changed

+126
-20
lines changed

src/Response/SapiStreamEmitter.php

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,18 @@ private function emitBody(ResponseInterface $response, $maxBufferLength)
5858
{
5959
$body = $response->getBody();
6060

61-
if (! $body->isSeekable()) {
62-
echo $body;
63-
return;
61+
if ($body->isSeekable()) {
62+
$body->rewind();
6463
}
6564

66-
$body->rewind();
67-
while (! $body->eof()) {
68-
echo $body->read($maxBufferLength);
65+
if ($body->isReadable())
66+
{
67+
while (! $body->eof())
68+
echo $body->read($maxBufferLength);
69+
return;
6970
}
71+
72+
echo $body;
7073
}
7174

7275
/**
@@ -82,25 +85,31 @@ private function emitBodyRange(array $range, ResponseInterface $response, $maxBu
8285

8386
$body = $response->getBody();
8487

85-
if (! $body->isSeekable()) {
86-
$contents = $body->getContents();
87-
echo substr($contents, $first, $last - $first + 1);
88-
return;
88+
$length = $last - $first + 1;
89+
90+
if ($body->isSeekable()) {
91+
$body->seek($first);
92+
$first = 0;
8993
}
9094

91-
$body = new RelativeStream($body, $first);
92-
$body->rewind();
93-
$pos = 0;
94-
$length = $last - $first + 1;
95-
while (! $body->eof() && $pos < $length) {
96-
if (($pos + $maxBufferLength) > $length) {
97-
echo $body->read($length - $pos);
98-
break;
95+
if ($body->isReadable())
96+
{
97+
for ($remaining = $length;
98+
($remaining >= $maxBufferLength) && (!$body->eof());
99+
$remaining -= strlen($contents)) {
100+
echo ($contents = $body->read($maxBufferLength));
101+
102+
}
103+
104+
if (($remaining > 0) && (!$body->eof())) {
105+
echo $body->read($remaining);
99106
}
100107

101-
echo $body->read($maxBufferLength);
102-
$pos = $body->tell();
108+
return;
103109
}
110+
111+
$contents = $body->getContents();
112+
echo substr($contents, $first, $length);
104113
}
105114

106115
/**

test/Response/SapiStreamEmitterTest.php

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
namespace ZendTest\Diactoros\Response;
1111

1212
use PHPUnit_Framework_TestCase as TestCase;
13+
use Prophecy\Argument;
1314
use Psr\Http\Message\ResponseInterface;
1415
use Psr\Http\Message\StreamInterface;
1516
use Zend\Diactoros\CallbackStream;
@@ -43,6 +44,7 @@ public function testDoesNotInjectContentLengthHeaderIfStreamSizeIsUnknown()
4344
$stream = $this->prophesize('Psr\Http\Message\StreamInterface');
4445
$stream->__toString()->willReturn('Content!');
4546
$stream->isSeekable()->willReturn(false);
47+
$stream->isReadable()->willReturn(false);
4648
$stream->eof()->willReturn(true);
4749
$stream->rewind()->willReturn(true);
4850
$stream->getSize()->willReturn(null);
@@ -58,6 +60,101 @@ public function testDoesNotInjectContentLengthHeaderIfStreamSizeIsUnknown()
5860
}
5961
}
6062

63+
public function emitBodyProvider()
64+
{
65+
return [
66+
[true, '01234567890123456789' , 10, 2],
67+
[true, '012345678901234567890123', 10, 3],
68+
[false, '01234567890123456789' , 10, 2],
69+
[false, '012345678901234567890123', 10, 3],
70+
];
71+
}
72+
73+
/**
74+
* @dataProvider emitBodyProvider
75+
*/
76+
public function testEmitBody($seekable, $contents, $maxBufferLength, $expectedReads = 0)
77+
{
78+
$position = 0;
79+
80+
$stream = $this->prophesize('Psr\Http\Message\StreamInterface');
81+
$stream->getSize()->willReturn(strlen($contents));
82+
$stream->isSeekable()->willReturn($seekable);
83+
$stream->isReadable()->willReturn(true);
84+
$stream->rewind()->willReturn(true);
85+
86+
$stream->eof()->will(function () use (&$contents, &$position){
87+
return !isset($contents[$position]);
88+
});
89+
90+
$stream->read(Argument::type('integer'))->will(function ($args) use (&$contents, &$position){
91+
$data = substr($contents, $position, $args[0]);
92+
$position += strlen($data);
93+
return $data;
94+
});
95+
96+
$response = (new Response())
97+
->withStatus(200)
98+
->withBody($stream->reveal());
99+
100+
ob_start();
101+
$this->emitter->emit($response, $maxBufferLength);
102+
$stream->rewind()->shouldBeCalledTimes($seekable ? 1 : 0);
103+
$stream->read(Argument::type('integer'))->shouldBeCalledTimes($expectedReads);
104+
$this->assertEquals($contents, ob_get_clean());
105+
106+
}
107+
108+
public function emitBodyRangeProvider()
109+
{
110+
return [
111+
[true, '01234567890123456789' , ['bytes', 10, 20, '*'], 10, 1],
112+
[true, '012345678901234567890123', ['bytes', 10, 40, '*'], 10, 2],
113+
[false, '01234567890123456789' , ['bytes', 11, 20, '*'], 10, 1],
114+
[false, '012345678901234567890123', ['bytes', 11, 40, '*'], 10, 2],
115+
];
116+
}
117+
118+
/**
119+
* @dataProvider emitBodyRangeProvider
120+
*/
121+
public function testEmitBodyRange($seekable, $contents, $range, $maxBufferLength, $expectedReads = 0)
122+
{
123+
list($unit, $first, $last, $length) = $range;
124+
125+
$position = $first;
126+
127+
$stream = $this->prophesize('Psr\Http\Message\StreamInterface');
128+
$stream->getSize()->willReturn(strlen($contents));
129+
$stream->isSeekable()->willReturn($seekable);
130+
$stream->isReadable()->willReturn(true);
131+
$stream->seek(Argument::type('integer'))->will(function ($args) use (&$position){
132+
$position = $args[0];
133+
return true;
134+
});
135+
136+
$stream->eof()->will(function () use (&$contents, &$position){
137+
return !isset($contents[$position]);
138+
});
139+
140+
$stream->read(Argument::type('integer'))->will(function ($args) use (&$contents, &$position){
141+
$data = substr($contents, $position, $args[0]);
142+
$position += strlen($data);
143+
return $data;
144+
});
145+
146+
$response = (new Response())
147+
->withStatus(200)
148+
->withHeader('Content-Range', "$unit $first-$last/$length")
149+
->withBody($stream->reveal());
150+
151+
ob_start();
152+
$this->emitter->emit($response, $maxBufferLength);
153+
$stream->seek(Argument::type('integer'))->shouldBeCalledTimes($seekable ? 1 : 0);
154+
$stream->read(Argument::type('integer'))->shouldBeCalledTimes($expectedReads);
155+
$this->assertEquals(substr($contents, $first, $last - $first + 1), ob_get_clean());
156+
}
157+
61158
public function contentRangeProvider()
62159
{
63160
return [

0 commit comments

Comments
 (0)