From 5805bc76205541b6254dec382c31b0d1d1b29803 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 2 Oct 2026 16:11:20 +0200 Subject: [PATCH] Stop output buffer cleanup at buffers that cannot be removed zlib.output_compression makes its buffer non-removable once it has sent compressed output, so ob_end_clean() raised a notice that replaced the original exception. Fixes #704. --- src/Whoops/Run.php | 6 ++-- src/Whoops/Util/SystemFacade.php | 10 +++++++ tests/Whoops/RunTest.php | 40 ++++++++++++++++++++++++++ tests/Whoops/Util/SystemFacadeTest.php | 19 ++++++++++++ 4 files changed, 73 insertions(+), 2 deletions(-) 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');