diff --git a/src/Whoops/Run.php b/src/Whoops/Run.php index b6eee28..73d021f 100644 --- a/src/Whoops/Run.php +++ b/src/Whoops/Run.php @@ -425,8 +425,10 @@ final class Run implements RunInterface if ($this->writeToOutput()) { // @todo Might be able to clean this up a bit better if ($willQuit) { - // Cleanup all other output buffers before sending our output: - while ($this->system->getOutputBufferLevel() > 0) { + // Cleanup all other output buffers before sending our output. + // Stop at a buffer that cannot be removed, e.g. zlib output + // compression once it has started sending compressed output: + while ($this->system->getOutputBufferLevel() > 0 && $this->system->isOutputBufferRemovable()) { $this->system->endOutputBuffering(); } diff --git a/src/Whoops/Util/SystemFacade.php b/src/Whoops/Util/SystemFacade.php index 9eb0acf..7025cb3 100644 --- a/src/Whoops/Util/SystemFacade.php +++ b/src/Whoops/Util/SystemFacade.php @@ -85,6 +85,16 @@ class SystemFacade return ob_get_level(); } + /** + * @return bool + */ + public function isOutputBufferRemovable() + { + $status = ob_get_status(); + + return isset($status['flags']) && ($status['flags'] & PHP_OUTPUT_HANDLER_REMOVABLE); + } + /** * @return bool */ diff --git a/tests/Whoops/RunTest.php b/tests/Whoops/RunTest.php index 708fa21..6c3c844 100755 --- a/tests/Whoops/RunTest.php +++ b/tests/Whoops/RunTest.php @@ -479,6 +479,46 @@ class RunTest extends TestCase $this->assertEquals("", ob_get_clean()); } + /** + * @covers Whoops\Run::handleException + */ + public function testOutputBufferCleanupStopsAtNonRemovableBuffer() + { + // A removable buffer on top of one that cannot be removed, like + // zlib.output_compression once it has sent compressed output. + $removable = [false, true]; + $test = $this; + + $system = m::mock('Whoops\Util\SystemFacade')->makePartial(); + $system->shouldReceive('stopExecution'); + $system->shouldReceive('getOutputBufferLevel') + ->andReturnUsing(function () use (&$removable) { + return count($removable); + }); + $system->shouldReceive('isOutputBufferRemovable') + ->andReturnUsing(function () use (&$removable) { + return end($removable); + }); + $system->shouldReceive('endOutputBuffering') + ->andReturnUsing(function () use ($test, &$removable) { + if (!array_pop($removable)) { + $test->fail('Tried to end an output buffer that cannot be removed'); + } + return true; + }); + + $run = new Run($system); + $run->pushHandler(function () { + echo "hello there"; + return Handler::QUIT; + }); + + ob_start(); + $run->handleException(new RuntimeException()); + $this->assertEquals("hello there", ob_get_clean()); + $this->assertEquals([false], $removable); + } + /** * @covers Whoops\Run::sendHttpCode */ diff --git a/tests/Whoops/Util/SystemFacadeTest.php b/tests/Whoops/Util/SystemFacadeTest.php index 554bbbf..c4d5827 100644 --- a/tests/Whoops/Util/SystemFacadeTest.php +++ b/tests/Whoops/Util/SystemFacadeTest.php @@ -68,6 +68,20 @@ class SystemFacadeTest extends TestCase $this->facade->endOutputBuffering(); } + public function test_it_reports_a_removable_output_buffer() + { + self::$runtime->shouldReceive('ob_get_status')->once()->andReturn(['flags' => PHP_OUTPUT_HANDLER_STDFLAGS]); + + $this->assertTrue($this->facade->isOutputBufferRemovable()); + } + + public function test_it_reports_a_non_removable_output_buffer() + { + self::$runtime->shouldReceive('ob_get_status')->once()->andReturn(['flags' => PHP_OUTPUT_HANDLER_STDFLAGS & ~PHP_OUTPUT_HANDLER_REMOVABLE]); + + $this->assertFalse($this->facade->isOutputBufferRemovable()); + } + public function test_it_delegates_flushing_the_current_buffer_to_the_native_implementation() { self::$runtime->shouldReceive('flush')->once(); @@ -161,6 +175,11 @@ function ob_end_clean() return SystemFacadeTest::delegate('ob_end_clean'); } +function ob_get_status() +{ + return SystemFacadeTest::delegate('ob_get_status'); +} + function flush() { return SystemFacadeTest::delegate('flush');