Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[9.x] Return non-zero exit code for uncaught exceptions #46541

Merged
merged 12 commits into from
Apr 6, 2023
6 changes: 5 additions & 1 deletion src/Illuminate/Foundation/Bootstrap/HandleExceptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,15 @@ public function handleException(Throwable $e)
try {
$this->getExceptionHandler()->report($e);
} catch (Exception $e) {
//
$exceptionHandlerFailed = true;
}

if (static::$app->runningInConsole()) {
$this->renderForConsole($e);

if ($exceptionHandlerFailed ?? false) {
exit(1);
}
} else {
$this->renderHttpResponse($e);
}
Expand Down
44 changes: 44 additions & 0 deletions tests/Integration/Foundation/ExceptionHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,21 @@
use Illuminate\Auth\Access\Response;
use Illuminate\Support\Facades\Route;
use Orchestra\Testbench\TestCase;
use Symfony\Component\Process\PhpProcess;

class ExceptionHandlerTest extends TestCase
{
/**
* Resolve application HTTP exception handler.
*
* @param \Illuminate\Foundation\Application $app
* @return void
*/
protected function resolveApplicationExceptionHandler($app)
{
$app->singleton('Illuminate\Contracts\Debug\ExceptionHandler', 'Illuminate\Foundation\Exceptions\Handler');
}

public function testItRendersAuthorizationExceptions()
{
Route::get('test-route', fn () => Response::deny('expected message', 321)->authorize());
Expand Down Expand Up @@ -107,4 +119,36 @@ public function testItHasFallbackErrorMessageForUnknownStatusCodes()
'message' => 'Whoops, looks like something went wrong.',
]);
}

/**
* @dataProvider exitCodesProvider
*/
public function testItReturnsNonZeroExitCodesForUncaughtExceptions($providers, $successful)
{
$basePath = static::applicationBasePath();
$providers = json_encode($providers, true);

$process = new PhpProcess(<<<EOF
<?php

require 'vendor/autoload.php';

\$laravel = Orchestra\Testbench\Foundation\Application::create(basePath: '$basePath', options: ['extra' => ['providers' => $providers]]);
\$laravel->singleton('Illuminate\Contracts\Debug\ExceptionHandler', 'Illuminate\Foundation\Exceptions\Handler');

\$kernel = \$laravel[Illuminate\Contracts\Console\Kernel::class];

return \$kernel->call('throw-exception-command');
EOF, __DIR__.'/../../../', ['APP_RUNNING_IN_CONSOLE' => true]);

$process->run();

$this->assertSame($successful, $process->isSuccessful());
}

public static function exitCodesProvider()
{
yield 'Throw exception' => [[Fixtures\Providers\ThrowUncaughtExceptionServiceProvider::class], false];
yield 'Do not throw exception' => [[Fixtures\Providers\ThrowExceptionServiceProvider::class], true];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Illuminate\Tests\Integration\Foundation\Fixtures\Console;

use Exception;
use Illuminate\Console\Command;

class ThrowExceptionCommand extends Command
{
protected $signature = 'throw-exception-command';

public function handle()
{
throw new Exception('Thrown inside ThrowExceptionCommand');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Illuminate\Tests\Integration\Foundation\Fixtures\Logs;

use Exception;
use Monolog\Handler\AbstractProcessingHandler;

class ThrowExceptionLogHandler extends AbstractProcessingHandler
{
protected function write(array $record): void
{
throw new Exception('Thrown inside ThrowExceptionLogHandler');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Illuminate\Tests\Integration\Foundation\Fixtures\Providers;

use Illuminate\Console\Application;
use Illuminate\Support\ServiceProvider;
use Illuminate\Tests\Integration\Foundation\Fixtures\Console\ThrowExceptionCommand;

class ThrowExceptionServiceProvider extends ServiceProvider
{
public function boot()
{
Application::starting(function ($artisan) {
$artisan->add(new ThrowExceptionCommand);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Illuminate\Tests\Integration\Foundation\Fixtures\Providers;

use Illuminate\Console\Application;
use Illuminate\Support\ServiceProvider;
use Illuminate\Tests\Integration\Foundation\Fixtures\Console\ThrowExceptionCommand;
use Illuminate\Tests\Integration\Foundation\Fixtures\Logs\ThrowExceptionLogHandler;

class ThrowUncaughtExceptionServiceProvider extends ServiceProvider
{
public function register()
{
$config = $this->app['config'];

$config->set('logging.default', 'throw_exception');

$config->set('logging.channels.throw_exception', [
'driver' => 'monolog',
'handler' => ThrowExceptionLogHandler::class,
]);
}

public function boot()
{
Application::starting(function ($artisan) {
$artisan->add(new ThrowExceptionCommand);
});
}
}