Skip to content

Commit dda4e0f

Browse files
[10.x] Do not bubble exceptions thrown rendering error view when debug is false (prevent infinite loops) (#48732)
* Do not bubble exceptions thrown rendering error view when debug is false * Add failing tests * Only re-throw when debug mode it `true` * Manually report exception if debug mode is `false` * Remove unit tests * lint * Revert "Remove unit tests" This reverts commit f4222ef. * Fix original unit tests * Fix paths for windows --------- Co-authored-by: Tim MacDonald <[email protected]>
1 parent 041aa19 commit dda4e0f

File tree

5 files changed

+103
-5
lines changed

5 files changed

+103
-5
lines changed

src/Illuminate/Foundation/Exceptions/Handler.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -719,10 +719,16 @@ protected function renderHttpException(HttpExceptionInterface $e)
719719
$this->registerErrorViewPaths();
720720

721721
if ($view = $this->getHttpExceptionView($e)) {
722-
return response()->view($view, [
723-
'errors' => new ViewErrorBag,
724-
'exception' => $e,
725-
], $e->getStatusCode(), $e->getHeaders());
722+
try {
723+
return response()->view($view, [
724+
'errors' => new ViewErrorBag,
725+
'exception' => $e,
726+
], $e->getStatusCode(), $e->getHeaders());
727+
} catch (Throwable $t) {
728+
config('app.debug') && throw $t;
729+
730+
$this->report($t);
731+
}
726732
}
727733

728734
return $this->convertExceptionToResponse($e);

tests/Foundation/FoundationExceptionsHandlerTest.php

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ class FoundationExceptionsHandlerTest extends TestCase
4949

5050
protected $config;
5151

52+
protected $viewFactory;
53+
5254
protected $container;
5355

5456
protected $handler;
@@ -59,14 +61,18 @@ protected function setUp(): void
5961
{
6062
$this->config = m::mock(Config::class);
6163

64+
$this->viewFactory = m::mock(ViewFactory::class);
65+
6266
$this->request = m::mock(stdClass::class);
6367

6468
$this->container = Container::setInstance(new Container);
6569

6670
$this->container->instance('config', $this->config);
6771

72+
$this->container->instance(ViewFactory::class, $this->viewFactory);
73+
6874
$this->container->instance(ResponseFactoryContract::class, new ResponseFactory(
69-
m::mock(ViewFactory::class),
75+
$this->viewFactory,
7076
m::mock(Redirector::class)
7177
));
7278

@@ -397,6 +403,43 @@ public function getErrorView($e)
397403
$this->assertNull($handler->getErrorView(new HttpException(404)));
398404
}
399405

406+
private function executeScenarioWhereErrorViewThrowsWhileRenderingAndDebugIs($debug)
407+
{
408+
$this->viewFactory->shouldReceive('exists')->once()->with('errors::404')->andReturn(true);
409+
$this->viewFactory->shouldReceive('make')->once()->withAnyArgs()->andThrow(new Exception("Rendering this view throws an exception"));
410+
411+
$this->config->shouldReceive('get')->with('app.debug', null)->andReturn($debug);
412+
413+
$handler = new class($this->container) extends Handler
414+
{
415+
protected function registerErrorViewPaths() {}
416+
417+
public function getErrorView($e)
418+
{
419+
return $this->renderHttpException($e);
420+
}
421+
};
422+
423+
$this->assertInstanceOf(SymfonyResponse::class, $handler->getErrorView(new HttpException(404)));
424+
}
425+
426+
public function testItDoesNotCrashIfErrorViewThrowsWhileRenderingAndDebugFalse()
427+
{
428+
// When debug is false, the exception thrown while rendering the error view
429+
// should not bubble as this may trigger an infinite loop.
430+
431+
}
432+
433+
public function testItDoesNotCrashIfErrorViewThrowsWhileRenderingAndDebugTrue()
434+
{
435+
// When debug is true, it is OK to bubble the exception thrown while rendering
436+
// the error view as the debug handler should handle this gracefully.
437+
438+
$this->expectException(\Exception::class);
439+
$this->expectExceptionMessage("Rendering this view throws an exception");
440+
$this->executeScenarioWhereErrorViewThrowsWhileRenderingAndDebugIs(true);
441+
}
442+
400443
public function testAssertExceptionIsThrown()
401444
{
402445
$this->assertThrows(function () {

tests/Integration/Foundation/ExceptionHandlerTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44

55
use Illuminate\Auth\Access\AuthorizationException;
66
use Illuminate\Auth\Access\Response;
7+
use Illuminate\Contracts\Debug\ExceptionHandler;
8+
use Illuminate\Support\Facades\Config;
79
use Illuminate\Support\Facades\Route;
810
use Orchestra\Testbench\TestCase;
911
use Symfony\Component\Process\PhpProcess;
12+
use Throwable;
1013

1114
class ExceptionHandlerTest extends TestCase
1215
{
@@ -151,4 +154,46 @@ public static function exitCodesProvider()
151154
yield 'Throw exception' => [[Fixtures\Providers\ThrowUncaughtExceptionServiceProvider::class], false];
152155
yield 'Do not throw exception' => [[Fixtures\Providers\ThrowExceptionServiceProvider::class], true];
153156
}
157+
158+
public function test_it_handles_malformed_error_views_in_production()
159+
{
160+
Config::set('view.paths', [__DIR__.'/Fixtures/MalformedErrorViews']);
161+
Config::set('app.debug', false);
162+
$reported = [];
163+
$this->app[ExceptionHandler::class]->reportable(function (Throwable $e) use (&$reported) {
164+
$reported[] = $e;
165+
});
166+
167+
try {
168+
$response = $this->get('foo');
169+
} catch (Throwable) {
170+
$response ??= null;
171+
}
172+
173+
$this->assertCount(1, $reported);
174+
$this->assertSame('Undefined variable $foo (View: '.__DIR__.DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR.'MalformedErrorViews'.DIRECTORY_SEPARATOR.'errors'.DIRECTORY_SEPARATOR.'404.blade.php)', $reported[0]->getMessage());
175+
$this->assertNotNull($response);
176+
$response->assertStatus(404);
177+
}
178+
179+
public function test_it_handles_malformed_error_views_in_development()
180+
{
181+
Config::set('view.paths', [__DIR__.'/Fixtures/MalformedErrorViews']);
182+
Config::set('app.debug', true);
183+
$reported = [];
184+
$this->app[ExceptionHandler::class]->reportable(function (Throwable $e) use (&$reported) {
185+
$reported[] = $e;
186+
});
187+
188+
try {
189+
$response = $this->get('foo');
190+
} catch (Throwable) {
191+
$response ??= null;
192+
}
193+
194+
$this->assertCount(1, $reported);
195+
$this->assertSame('Undefined variable $foo (View: '.__DIR__.DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR.'MalformedErrorViews'.DIRECTORY_SEPARATOR.'errors'.DIRECTORY_SEPARATOR.'404.blade.php)', $reported[0]->getMessage());
196+
$this->assertNotNull($response);
197+
$response->assertStatus(500);
198+
}
154199
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
My custom 404
2+
<?php $foo();
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
My custom 500
2+
<?php $bar();

0 commit comments

Comments
 (0)