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

Removes Content-Length injection and output buffer flushing #270

Merged
merged 4 commits into from
Sep 13, 2017
Merged
Show file tree
Hide file tree
Changes from 2 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
10 changes: 2 additions & 8 deletions src/Response/SapiEmitter.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,13 @@ class SapiEmitter implements EmitterInterface
* body content via the output buffer.
*
* @param ResponseInterface $response
* @param null|int $maxBufferLevel Maximum output buffering level to unwrap.
*/
public function emit(ResponseInterface $response, $maxBufferLevel = null)
public function emit(ResponseInterface $response)
{
if (headers_sent()) {
throw new RuntimeException('Unable to emit response; headers already sent');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this removed? You won't be able to emit the status line or headers if output has already been started - the first call to header() will error. Isn't it better to have the exception, which explains why?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, never mind, you just moved it somewhere else ;-)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😄

}

$response = $this->injectContentLength($response);
$this->assertNoPreviousOutput();

$this->emitStatusLine($response);
$this->emitHeaders($response);
$this->flush($maxBufferLevel);
$this->emitBody($response);
}

Expand Down
41 changes: 13 additions & 28 deletions src/Response/SapiEmitterTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,28 @@
namespace Zend\Diactoros\Response;

use Psr\Http\Message\ResponseInterface;
use RuntimeException;

trait SapiEmitterTrait
{
/**
* Inject the Content-Length header if is not already present.
* Checks to see if content has previously been sent.
*
* @param ResponseInterface $response
* @return ResponseInterface
* If either headers have been sent or the output buffer contains content,
* raises an exception.
*
* @throws RuntimeException if headers have already been sent.
* @throws RuntimeException if output is present in the output buffer.
*/
private function injectContentLength(ResponseInterface $response)
private function assertNoPreviousOutput()
{
if (! $response->hasHeader('Content-Length')) {
// PSR-7 indicates int OR null for the stream size; for null values,
// we will not auto-inject the Content-Length.
if (null !== $response->getBody()->getSize()) {
return $response->withHeader('Content-Length', (string) $response->getBody()->getSize());
}
if (headers_sent()) {
throw new RuntimeException('Unable to emit response; headers already sent');
}

return $response;
if (ob_get_level() > 0 && ob_get_length() > 0) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mindplay-dk: note that I separated out this from the headers_sent() check; I did this to allow having discretely different exception messages.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line 32: the PR is called 'Remove output buffer awareness', but line 32 is checking the output buffer :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, so is calling headers_sent(). :)

I suppose I should retitle it "remove output buffer flushing"...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-named the PR to "Removes Content-Length injection and output buffer flushing".

throw new RuntimeException('Output has been emitted previously; cannot emit response');
}
}

/**
Expand Down Expand Up @@ -77,23 +79,6 @@ private function emitHeaders(ResponseInterface $response)
}
}

/**
* Loops through the output buffer, flushing each, before emitting
* the response.
*
* @param int|null $maxBufferLevel Flush up to this buffer level.
*/
private function flush($maxBufferLevel = null)
{
if (null === $maxBufferLevel) {
$maxBufferLevel = ob_get_level();
}

while (ob_get_level() > $maxBufferLevel) {
ob_end_flush();
}
}

/**
* Filter a header name to wordcase
*
Expand Down
11 changes: 2 additions & 9 deletions src/Response/SapiStreamEmitter.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,13 @@ class SapiStreamEmitter implements EmitterInterface
* body content via the output buffer.
*
* @param ResponseInterface $response
* @param null|int $maxBufferLevel Maximum output buffering level to unwrap.
* @param int $maxBufferLength Maximum output buffering size for each iteration
*/
public function emit(ResponseInterface $response, $maxBufferLevel = null, $maxBufferLength = 8192)
public function emit(ResponseInterface $response, $maxBufferLength = 8192)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ironically, we changed the signature of this method to swap the $maxBufferLength and $maxBufferLevel arguments for 1.5.0, fixing an issue whereby 2 bytes at a time were streamed; had I resolved this issue prior to that release, we would not have needed to make that change, and then revert it here.

{
if (headers_sent()) {
throw new RuntimeException('Unable to emit response; headers already sent');
}

$response = $this->injectContentLength($response);

$this->assertNoPreviousOutput();
$this->emitStatusLine($response);
$this->emitHeaders($response);
$this->flush($maxBufferLevel);

$range = $this->parseContentRange($response->getHeaderLine('Content-Range'));

Expand Down
10 changes: 2 additions & 8 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,24 +150,18 @@ public static function createServerFromRequest(
* If provided a $finalHandler, that callable will be used for
* incomplete requests.
*
* Output buffering is enabled prior to invoking the attached
* callback; any output buffered will be sent prior to any
* response body content.
*
* @param null|callable $finalHandler
*/
public function listen(callable $finalHandler = null)
{
$callback = $this->callback;

ob_start();
$bufferLevel = ob_get_level();

$response = $callback($this->request, $this->response, $finalHandler);
if (! $response instanceof ResponseInterface) {
$response = $this->response;
}
$this->getEmitter()->emit($response, $bufferLevel);

$this->getEmitter()->emit($response);
}

/**
Expand Down
1 change: 0 additions & 1 deletion test/Response/AbstractEmitterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ public function testEmitsResponseHeaders()
ob_end_clean();
$this->assertContains('HTTP/1.1 200 OK', HeaderStack::stack());
$this->assertContains('Content-Type: text/plain', HeaderStack::stack());
$this->assertContains('Content-Length: 8', HeaderStack::stack());
}

public function testEmitsMessageBody()
Expand Down
22 changes: 0 additions & 22 deletions test/Response/SapiEmitterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,4 @@

class SapiEmitterTest extends AbstractEmitterTest
{
public function testEmitsBufferLevel()
{
ob_start();
echo "level" . ob_get_level() . " "; // 2
ob_start();
echo "level" . ob_get_level() . " "; // 3
ob_start();
echo "level" . ob_get_level() . " "; // 4
$response = (new Response())
->withStatus(200)
->withAddedHeader('Content-Type', 'text/plain');
$response->getBody()->write('Content!');
ob_start();
$this->emitter->emit($response);
$this->assertEquals('Content!', ob_get_contents());
ob_end_clean();
$this->assertEquals('level4 ', ob_get_contents(), 'current buffer level string must remains after emit');
ob_end_clean();
$this->emitter->emit($response, 2);
$this->assertEquals('level2 level3 Content!', ob_get_contents(), 'must buffer until specified level');
ob_end_clean();
}
}
6 changes: 3 additions & 3 deletions test/Response/SapiStreamEmitterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ function ($bufferLength) use (& $peakBufferLength) {
->withBody($stream->reveal());

ob_start();
$this->emitter->emit($response, null, $maxBufferLength);
$this->emitter->emit($response, $maxBufferLength);
$emittedContents = ob_get_clean();

if ($seekable) {
Expand Down Expand Up @@ -351,7 +351,7 @@ function ($bufferLength) use (& $peakBufferLength) {
->withBody($stream->reveal());

ob_start();
$this->emitter->emit($response, null, $maxBufferLength);
$this->emitter->emit($response, $maxBufferLength);
$emittedContents = ob_get_clean();

$stream->rewind()->shouldNotBeCalled();
Expand Down Expand Up @@ -497,7 +497,7 @@ function () use (& $closureTrackMemoryUsage) {

gc_disable();

$this->emitter->emit($response, null, $maxBufferLength);
$this->emitter->emit($response, $maxBufferLength);

ob_end_flush();

Expand Down
49 changes: 0 additions & 49 deletions test/ServerIntegrationTest.php

This file was deleted.

7 changes: 0 additions & 7 deletions test/ServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ public function testEmitterSetter()

$this->expectOutputString('');
$server->listen();
ob_end_flush();
}

public function testCreateServerWillCreateDefaultInstancesForRequestAndResponse()
Expand Down Expand Up @@ -153,7 +152,6 @@ public function testListenInvokesCallbackAndSendsResponse()

$this->expectOutputString('FOOBAR');
$server->listen();
ob_end_flush();

$this->assertContains('HTTP/1.1 200 OK', HeaderStack::stack());
$this->assertContains('Content-Type: text/plain', HeaderStack::stack());
Expand All @@ -179,7 +177,6 @@ public function testListenEmitsStatusHeaderWithoutReasonPhraseIfNoReasonPhrase()

$this->expectOutputString('FOOBAR');
$server->listen();
ob_end_flush();

$this->assertContains('HTTP/1.1 299', HeaderStack::stack());
$this->assertContains('Content-Type: text/plain', HeaderStack::stack());
Expand All @@ -204,7 +201,6 @@ public function testEnsurePercentCharactersDoNotResultInOutputError()

$this->expectOutputString('100%');
$server->listen();
ob_end_flush();

$this->assertContains('HTTP/1.1 200 OK', HeaderStack::stack());
$this->assertContains('Content-Type: text/plain', HeaderStack::stack());
Expand Down Expand Up @@ -233,7 +229,6 @@ public function testEmitsHeadersWithMultipleValuesMultipleTimes()
$server = Server::createServer($callback, $server, [], [], [], []);

$server->listen();
ob_end_flush();

$this->assertContains('HTTP/1.1 200 OK', HeaderStack::stack());
$this->assertContains('Content-Type: text/plain', HeaderStack::stack());
Expand All @@ -256,7 +251,6 @@ public function testEmitsHeadersWithMultipleValuesMultipleTimes()
*/
public function testHeaderOrderIsHonoredWhenEmitted($stack)
{
array_pop($stack); // ignore "Content-Length" automatically set by the response emitter
$header = array_pop($stack);
$this->assertContains(
'Set-Cookie: bar=baz; expires=Wed, 8 Oct 2014 10:30; path=/foo/bar; domain=example.com',
Expand Down Expand Up @@ -311,7 +305,6 @@ public function testListenPassesCallableArgumentToCallback()
$this->response
);
$server->listen($final);
ob_end_flush();
$this->assertTrue($invoked);
}
}