Merge pull request #795 from dualfroz/fix/704-non-removable-output-buffer

Stop output buffer cleanup at buffers that cannot be removed
This commit is contained in:
Denis Sokolov
2026-10-03 19:03:57 +02:00
committed by GitHub
4 changed files with 73 additions and 2 deletions
+4 -2
View File
@@ -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();
}
+10
View File
@@ -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
*/
+40
View File
@@ -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
*/
+19
View File
@@ -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');